diff options
Diffstat (limited to 'pkg')
78 files changed, 2936 insertions, 569 deletions
diff --git a/pkg/api/handlers/compat/containers.go b/pkg/api/handlers/compat/containers.go index 4f101ce84..ad341c3ab 100644 --- a/pkg/api/handlers/compat/containers.go +++ b/pkg/api/handlers/compat/containers.go @@ -356,11 +356,20 @@ func LibpodToContainer(l *libpod.Container, sz bool) (*handlers.Container, error return nil, err } + m, err := json.Marshal(inspect.Mounts) + if err != nil { + return nil, err + } + mounts := []types.MountPoint{} + if err := json.Unmarshal(m, &mounts); err != nil { + return nil, err + } + return &handlers.Container{Container: types.Container{ ID: l.ID(), Names: []string{fmt.Sprintf("/%s", l.Name())}, Image: imageName, - ImageID: imageID, + ImageID: "sha256:" + imageID, Command: strings.Join(l.Command(), " "), Created: l.CreatedTime().Unix(), Ports: ports, @@ -374,7 +383,7 @@ func LibpodToContainer(l *libpod.Container, sz bool) (*handlers.Container, error }{ "host"}, NetworkSettings: &networkSettings, - Mounts: nil, + Mounts: mounts, }, ContainerCreateConfig: types.ContainerCreateConfig{}, }, nil diff --git a/pkg/api/handlers/compat/events.go b/pkg/api/handlers/compat/events.go index 901acdac4..bc31a36c4 100644 --- a/pkg/api/handlers/compat/events.go +++ b/pkg/api/handlers/compat/events.go @@ -91,6 +91,8 @@ func GetEvents(w http.ResponseWriter, r *http.Request) { e := entities.ConvertToEntitiesEvent(*evt) if !utils.IsLibpodRequest(r) && e.Status == "died" { e.Status = "die" + e.Action = "die" + e.Actor.Attributes["exitCode"] = e.Actor.Attributes["containerExitCode"] } if err := coder.Encode(e); err != nil { diff --git a/pkg/api/handlers/compat/images_build.go b/pkg/api/handlers/compat/images_build.go index 2fd5dcccd..0fcac5330 100644 --- a/pkg/api/handlers/compat/images_build.go +++ b/pkg/api/handlers/compat/images_build.go @@ -621,7 +621,8 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { Stream string `json:"stream,omitempty"` Error *jsonmessage.JSONError `json:"errorDetail,omitempty"` // NOTE: `error` is being deprecated check https://github.com/moby/moby/blob/master/pkg/jsonmessage/jsonmessage.go#L148 - ErrorMessage string `json:"error,omitempty"` // deprecate this slowly + ErrorMessage string `json:"error,omitempty"` // deprecate this slowly + Aux json.RawMessage `json:"aux,omitempty"` }{} select { @@ -655,7 +656,12 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { return case <-runCtx.Done(): if success { - if !utils.IsLibpodRequest(r) { + if !utils.IsLibpodRequest(r) && !query.Quiet { + m.Aux = []byte(fmt.Sprintf(`{"ID":"sha256:%s"}`, imageID)) + if err := enc.Encode(m); err != nil { + logrus.Warnf("failed to json encode error %v", err) + } + m.Aux = nil m.Stream = fmt.Sprintf("Successfully built %12.12s\n", imageID) if err := enc.Encode(m); err != nil { logrus.Warnf("Failed to json encode error %v", err) diff --git a/pkg/api/handlers/compat/info.go b/pkg/api/handlers/compat/info.go index 941718a8b..777009f0a 100644 --- a/pkg/api/handlers/compat/info.go +++ b/pkg/api/handlers/compat/info.go @@ -84,7 +84,6 @@ func GetInfo(w http.ResponseWriter, r *http.Request) { InitBinary: "", InitCommit: docker.Commit{}, Isolation: "", - KernelMemory: sysInfo.KernelMemory, KernelMemoryTCP: false, KernelVersion: infoData.Host.Kernel, Labels: nil, diff --git a/pkg/api/handlers/compat/networks.go b/pkg/api/handlers/compat/networks.go index 8aab29658..db3af7d0b 100644 --- a/pkg/api/handlers/compat/networks.go +++ b/pkg/api/handlers/compat/networks.go @@ -299,20 +299,66 @@ func Connect(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) var ( - aliases []string netConnect types.NetworkConnect ) if err := json.NewDecoder(r.Body).Decode(&netConnect); err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()")) return } + + netOpts := nettypes.PerNetworkOptions{} + name := utils.GetName(r) if netConnect.EndpointConfig != nil { if netConnect.EndpointConfig.Aliases != nil { - aliases = netConnect.EndpointConfig.Aliases + netOpts.Aliases = netConnect.EndpointConfig.Aliases + } + + // if IP address is provided + if len(netConnect.EndpointConfig.IPAddress) > 0 { + staticIP := net.ParseIP(netConnect.EndpointConfig.IPAddress) + if staticIP == nil { + utils.Error(w, "failed to parse the ip address", http.StatusInternalServerError, + errors.Errorf("failed to parse the ip address %q", netConnect.EndpointConfig.IPAddress)) + return + } + netOpts.StaticIPs = append(netOpts.StaticIPs, staticIP) + } + + if netConnect.EndpointConfig.IPAMConfig != nil { + // if IPAMConfig.IPv4Address is provided + if len(netConnect.EndpointConfig.IPAMConfig.IPv4Address) > 0 { + staticIP := net.ParseIP(netConnect.EndpointConfig.IPAMConfig.IPv4Address) + if staticIP == nil { + utils.Error(w, "failed to parse the ipv4 address", http.StatusInternalServerError, + errors.Errorf("failed to parse the ipv4 address %q", netConnect.EndpointConfig.IPAMConfig.IPv4Address)) + return + } + netOpts.StaticIPs = append(netOpts.StaticIPs, staticIP) + } + // if IPAMConfig.IPv6Address is provided + if len(netConnect.EndpointConfig.IPAMConfig.IPv6Address) > 0 { + staticIP := net.ParseIP(netConnect.EndpointConfig.IPAMConfig.IPv6Address) + if staticIP == nil { + utils.Error(w, "failed to parse the ipv6 address", http.StatusInternalServerError, + errors.Errorf("failed to parse the ipv6 address %q", netConnect.EndpointConfig.IPAMConfig.IPv6Address)) + return + } + netOpts.StaticIPs = append(netOpts.StaticIPs, staticIP) + } + } + // If MAC address is provided + if len(netConnect.EndpointConfig.MacAddress) > 0 { + staticMac, err := net.ParseMAC(netConnect.EndpointConfig.MacAddress) + if err != nil { + utils.Error(w, "failed to parse the mac address", http.StatusInternalServerError, + errors.Errorf("failed to parse the mac address %q", netConnect.EndpointConfig.IPAMConfig.IPv6Address)) + return + } + netOpts.StaticMAC = nettypes.HardwareAddr(staticMac) } } - err := runtime.ConnectContainerToNetwork(netConnect.Container, name, aliases) + err := runtime.ConnectContainerToNetwork(netConnect.Container, name, netOpts) if err != nil { if errors.Cause(err) == define.ErrNoSuchCtr { utils.ContainerNotFound(w, netConnect.Container, err) diff --git a/pkg/api/handlers/compat/swagger.go b/pkg/api/handlers/compat/swagger.go index cfbdd1154..32167d6b5 100644 --- a/pkg/api/handlers/compat/swagger.go +++ b/pkg/api/handlers/compat/swagger.go @@ -55,15 +55,13 @@ type swagCompatNetworkCreateResponse struct { } // Network disconnect -// swagger:model NetworkConnectRequest +// swagger:model NetworkCompatConnectRequest type swagCompatNetworkConnectRequest struct { - // in:body - Body struct{ types.NetworkConnect } + types.NetworkConnect } // Network disconnect -// swagger:model NetworkDisconnectRequest +// swagger:model NetworkCompatDisconnectRequest type swagCompatNetworkDisconnectRequest struct { - // in:body - Body struct{ types.NetworkDisconnect } + types.NetworkDisconnect } diff --git a/pkg/api/handlers/libpod/containers_create.go b/pkg/api/handlers/libpod/containers_create.go index 77bfe7b50..d1841769a 100644 --- a/pkg/api/handlers/libpod/containers_create.go +++ b/pkg/api/handlers/libpod/containers_create.go @@ -19,11 +19,15 @@ import ( func CreateContainer(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) var sg specgen.SpecGenerator + if err := json.NewDecoder(r.Body).Decode(&sg); err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()")) return } - + if sg.Passwd == nil { + t := true + sg.Passwd = &t + } warn, err := generate.CompleteSpec(r.Context(), runtime, &sg) if err != nil { utils.InternalServerError(w, err) diff --git a/pkg/api/handlers/libpod/networks.go b/pkg/api/handlers/libpod/networks.go index 1f7f2e26c..a28c3c57c 100644 --- a/pkg/api/handlers/libpod/networks.go +++ b/pkg/api/handlers/libpod/networks.go @@ -125,7 +125,7 @@ func Connect(w http.ResponseWriter, r *http.Request) { return } name := utils.GetName(r) - err := runtime.ConnectContainerToNetwork(netConnect.Container, name, netConnect.Aliases) + err := runtime.ConnectContainerToNetwork(netConnect.Container, name, netConnect.PerNetworkOptions) if err != nil { if errors.Cause(err) == define.ErrNoSuchCtr { utils.ContainerNotFound(w, netConnect.Container, err) diff --git a/pkg/api/handlers/libpod/play.go b/pkg/api/handlers/libpod/play.go index e6ae9ad18..6ef83ad92 100644 --- a/pkg/api/handlers/libpod/play.go +++ b/pkg/api/handlers/libpod/play.go @@ -23,7 +23,7 @@ func PlayKube(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) query := struct { - Network string `schema:"network"` + Network []string `schema:"network"` TLSVerify bool `schema:"tlsVerify"` LogDriver string `schema:"logDriver"` LogOptions []string `schema:"logOptions"` @@ -103,7 +103,7 @@ func PlayKube(w http.ResponseWriter, r *http.Request) { Authfile: authfile, Username: username, Password: password, - Network: query.Network, + Networks: query.Network, NoHosts: query.NoHosts, Quiet: true, LogDriver: query.LogDriver, diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go index 3d18406a5..1b29831b4 100644 --- a/pkg/api/handlers/libpod/pods.go +++ b/pkg/api/handlers/libpod/pods.go @@ -42,6 +42,7 @@ func PodCreate(w http.ResponseWriter, r *http.Request) { infraOptions := entities.NewInfraContainerCreateOptions() // options for pulling the image and FillOutSpec infraOptions.Net = &entities.NetOptions{} infraOptions.Devices = psg.Devices + infraOptions.SecurityOpt = psg.SecurityOpt err = specgenutil.FillOutSpecGen(psg.InfraContainerSpec, &infraOptions, []string{}) // necessary for default values in many cases (userns, idmappings) if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error filling out specgen")) diff --git a/pkg/api/handlers/libpod/swagger.go b/pkg/api/handlers/libpod/swagger.go index 7ccfdd0f3..8d7058b1e 100644 --- a/pkg/api/handlers/libpod/swagger.go +++ b/pkg/api/handlers/libpod/swagger.go @@ -133,6 +133,12 @@ type swagNetworkPruneResponse struct { Body []entities.NetworkPruneReport } +// Network connect +// swagger:model NetworkConnectRequest +type swagNetworkConnectRequest struct { + entities.NetworkConnectOptions +} + func ServeSwagger(w http.ResponseWriter, r *http.Request) { path := DefaultPodmanSwaggerSpec if p, found := os.LookupEnv("PODMAN_SWAGGER_SPEC"); found { diff --git a/pkg/api/handlers/utils/images.go b/pkg/api/handlers/utils/images.go index d874165e3..3f3f48193 100644 --- a/pkg/api/handlers/utils/images.go +++ b/pkg/api/handlers/utils/images.go @@ -35,7 +35,7 @@ func NormalizeToDockerHub(r *http.Request, nameOrID string) (string, error) { if errors.Cause(err) != storage.ErrImageUnknown { return "", fmt.Errorf("normalizing name for compat API: %v", err) } - } else if strings.HasPrefix(img.ID(), nameOrID) { + } else if strings.HasPrefix(img.ID(), strings.TrimPrefix(nameOrID, "sha256:")) { return img.ID(), nil } diff --git a/pkg/api/server/docs.go b/pkg/api/server/docs.go index 83d9ef160..2127e7d82 100644 --- a/pkg/api/server/docs.go +++ b/pkg/api/server/docs.go @@ -1,4 +1,4 @@ -// Package api Provides an API for the Libpod library +// Package api Provides an API for the Libpod library // // This documentation describes the Podman v2.0 RESTful API. // It replaces the Podman v1.0 API and was initially delivered @@ -45,7 +45,7 @@ // Schemes: http, https // Host: podman.io // BasePath: / -// Version: 3.2.0 +// Version: 4.0.0 // License: Apache-2.0 https://opensource.org/licenses/Apache-2.0 // Contact: Podman <podman@lists.podman.io> https://podman.io/community/ // diff --git a/pkg/api/server/register_networks.go b/pkg/api/server/register_networks.go index 641bce333..77e8a80fd 100644 --- a/pkg/api/server/register_networks.go +++ b/pkg/api/server/register_networks.go @@ -101,7 +101,7 @@ func (s *APIServer) registerNetworkHandlers(r *mux.Router) error { // parameters: // - in: body // name: create - // description: attributes for creating a container + // description: attributes for creating a network // schema: // $ref: "#/definitions/NetworkCreateRequest" // responses: @@ -131,7 +131,7 @@ func (s *APIServer) registerNetworkHandlers(r *mux.Router) error { // name: create // description: attributes for connecting a container to a network // schema: - // $ref: "#/definitions/NetworkConnectRequest" + // $ref: "#/definitions/NetworkCompatConnectRequest" // responses: // 200: // description: OK @@ -159,7 +159,7 @@ func (s *APIServer) registerNetworkHandlers(r *mux.Router) error { // name: create // description: attributes for disconnecting a container from a network // schema: - // $ref: "#/definitions/NetworkDisconnectRequest" + // $ref: "#/definitions/NetworkCompatDisconnectRequest" // responses: // 200: // description: OK @@ -312,7 +312,7 @@ func (s *APIServer) registerNetworkHandlers(r *mux.Router) error { // parameters: // - in: body // name: create - // description: attributes for creating a container + // description: attributes for creating a network // schema: // $ref: "#/definitions/NetworkCreateLibpod" // responses: @@ -368,7 +368,7 @@ func (s *APIServer) registerNetworkHandlers(r *mux.Router) error { // name: create // description: attributes for disconnecting a container from a network // schema: - // $ref: "#/definitions/NetworkDisconnectRequest" + // $ref: "#/definitions/NetworkCompatDisconnectRequest" // responses: // 200: // description: OK diff --git a/pkg/api/server/register_play.go b/pkg/api/server/register_play.go index 915d0d02e..5ace01929 100644 --- a/pkg/api/server/register_play.go +++ b/pkg/api/server/register_play.go @@ -18,8 +18,10 @@ func (s *APIServer) registerPlayHandlers(r *mux.Router) error { // parameters: // - in: query // name: network - // type: string - // description: Connect the pod to this network. + // type: array + // description: USe the network mode or specify an array of networks. + // items: + // type: string // - in: query // name: tlsVerify // type: boolean diff --git a/pkg/api/server/register_volumes.go b/pkg/api/server/register_volumes.go index fb02cffcf..d1c1d5024 100644 --- a/pkg/api/server/register_volumes.go +++ b/pkg/api/server/register_volumes.go @@ -17,7 +17,7 @@ func (s *APIServer) registerVolumeHandlers(r *mux.Router) error { // parameters: // - in: body // name: create - // description: attributes for creating a container + // description: attributes for creating a volume // schema: // $ref: "#/definitions/VolumeCreate" // produces: @@ -188,7 +188,7 @@ func (s *APIServer) registerVolumeHandlers(r *mux.Router) error { // - in: body // name: create // description: | - // attributes for creating a container. + // attributes for creating a volume. // Note: If a volume by the same name exists, a 201 response with that volume's information will be generated. // schema: // $ref: "#/definitions/DockerVolumeCreate" diff --git a/pkg/bindings/network/network.go b/pkg/bindings/network/network.go index 172598be1..66e01a016 100644 --- a/pkg/bindings/network/network.go +++ b/pkg/bindings/network/network.go @@ -3,7 +3,6 @@ package network import ( "context" "net/http" - "net/url" "strings" "github.com/containers/podman/v3/libpod/network/types" @@ -110,8 +109,6 @@ func Disconnect(ctx context.Context, networkName string, ContainerNameOrID strin if err != nil { return err } - // No params are used for disconnect - params := url.Values{} // Disconnect sends everything in body disconnect := struct { Container string @@ -128,7 +125,7 @@ func Disconnect(ctx context.Context, networkName string, ContainerNameOrID strin return err } stringReader := strings.NewReader(body) - response, err := conn.DoRequest(ctx, stringReader, http.MethodPost, "/networks/%s/disconnect", params, nil, networkName) + response, err := conn.DoRequest(ctx, stringReader, http.MethodPost, "/networks/%s/disconnect", nil, nil, networkName) if err != nil { return err } @@ -138,32 +135,26 @@ func Disconnect(ctx context.Context, networkName string, ContainerNameOrID strin } // Connect adds a container to a network -func Connect(ctx context.Context, networkName string, ContainerNameOrID string, options *ConnectOptions) error { +func Connect(ctx context.Context, networkName string, containerNameOrID string, options *types.PerNetworkOptions) error { if options == nil { - options = new(ConnectOptions) + options = new(types.PerNetworkOptions) } conn, err := bindings.GetClient(ctx) if err != nil { return err } - // No params are used in connect - params := url.Values{} // Connect sends everything in body - connect := struct { - Container string - Aliases []string - }{ - Container: ContainerNameOrID, - } - if aliases := options.GetAliases(); options.Changed("Aliases") { - connect.Aliases = aliases + connect := entities.NetworkConnectOptions{ + Container: containerNameOrID, + PerNetworkOptions: *options, } + body, err := jsoniter.MarshalToString(connect) if err != nil { return err } stringReader := strings.NewReader(body) - response, err := conn.DoRequest(ctx, stringReader, http.MethodPost, "/networks/%s/connect", params, nil, networkName) + response, err := conn.DoRequest(ctx, stringReader, http.MethodPost, "/networks/%s/connect", nil, nil, networkName) if err != nil { return err } diff --git a/pkg/bindings/network/types.go b/pkg/bindings/network/types.go index 8088de061..b82c0e438 100644 --- a/pkg/bindings/network/types.go +++ b/pkg/bindings/network/types.go @@ -1,6 +1,8 @@ package network -import "net" +import ( + "net" +) //go:generate go run ../generator/generator.go CreateOptions // CreateOptions are optional options for creating networks @@ -61,15 +63,6 @@ type DisconnectOptions struct { Force *bool } -//go:generate go run ../generator/generator.go ConnectOptions -// ConnectOptions are optional options for connecting -// containers from a network -type ConnectOptions struct { - // Aliases are names the container will be known as - // when using the dns plugin - Aliases *[]string -} - //go:generate go run ../generator/generator.go ExistsOptions // ExistsOptions are optional options for checking // if a network exists diff --git a/pkg/bindings/network/types_connect_options.go b/pkg/bindings/network/types_connect_options.go deleted file mode 100644 index b7a465999..000000000 --- a/pkg/bindings/network/types_connect_options.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code generated by go generate; DO NOT EDIT. -package network - -import ( - "net/url" - - "github.com/containers/podman/v3/pkg/bindings/internal/util" -) - -// Changed returns true if named field has been set -func (o *ConnectOptions) Changed(fieldName string) bool { - return util.Changed(o, fieldName) -} - -// ToParams formats struct fields to be passed to API service -func (o *ConnectOptions) ToParams() (url.Values, error) { - return util.ToParams(o) -} - -// WithAliases set field Aliases to given value -func (o *ConnectOptions) WithAliases(value []string) *ConnectOptions { - o.Aliases = &value - return o -} - -// GetAliases returns value of field Aliases -func (o *ConnectOptions) GetAliases() []string { - if o.Aliases == nil { - var z []string - return z - } - return *o.Aliases -} diff --git a/pkg/bindings/play/types.go b/pkg/bindings/play/types.go index 011f7f9ca..ca639e46b 100644 --- a/pkg/bindings/play/types.go +++ b/pkg/bindings/play/types.go @@ -15,8 +15,8 @@ type KubeOptions struct { Username *string // Password for authenticating against the registry. Password *string - // Network - name of the CNI network to connect to. - Network *string + // Network - name of the networks to connect to. + Network *[]string // NoHosts - do not generate /etc/hosts file in pod's containers NoHosts *bool // Quiet - suppress output when pulling images. diff --git a/pkg/bindings/play/types_kube_options.go b/pkg/bindings/play/types_kube_options.go index 344771e0c..593f026a3 100644 --- a/pkg/bindings/play/types_kube_options.go +++ b/pkg/bindings/play/types_kube_options.go @@ -79,15 +79,15 @@ func (o *KubeOptions) GetPassword() string { } // WithNetwork set field Network to given value -func (o *KubeOptions) WithNetwork(value string) *KubeOptions { +func (o *KubeOptions) WithNetwork(value []string) *KubeOptions { o.Network = &value return o } // GetNetwork returns value of field Network -func (o *KubeOptions) GetNetwork() string { +func (o *KubeOptions) GetNetwork() []string { if o.Network == nil { - var z string + var z []string return z } return *o.Network diff --git a/pkg/bindings/test/images_test.go b/pkg/bindings/test/images_test.go index 8489e6ff1..4ee824472 100644 --- a/pkg/bindings/test/images_test.go +++ b/pkg/bindings/test/images_test.go @@ -85,14 +85,16 @@ var _ = Describe("Podman images", func() { // Test to validate the remove image api It("remove image", func() { - // Remove invalid image should be a 404 + // NOTE that removing an image that does not exist will still + // return a 200 http status. The response, however, includes + // the exit code that podman-remote should exit with. + // + // The libpod/images/remove endpoint supports batch removal of + // images for performance reasons and for hiding the logic of + // deciding which exit code to use from the client. response, errs := images.Remove(bt.conn, []string{"foobar5000"}, nil) Expect(len(errs)).To(BeNumerically(">", 0)) - code, _ := bindings.CheckResponseCode(errs[0]) - // FIXME FIXME FIXME: #12441: THIS IS BROKEN - // FIXME FIXME FIXME: we get msg: "foobar5000: image not known" - // FIXME FIXME FIXME: ...with no ResponseCode - Expect(code).To(BeNumerically("==", -1)) + Expect(response.ExitCode).To(BeNumerically("==", 1)) // podman-remote would exit with 1 // Remove an image by name, validate image is removed and error is nil inspectData, err := images.GetImage(bt.conn, busybox.shortName, nil) @@ -102,7 +104,7 @@ var _ = Describe("Podman images", func() { Expect(inspectData.ID).To(Equal(response.Deleted[0])) inspectData, err = images.GetImage(bt.conn, busybox.shortName, nil) - code, _ = bindings.CheckResponseCode(err) + code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) // Start a container with alpine image diff --git a/pkg/checkpoint/checkpoint_restore.go b/pkg/checkpoint/checkpoint_restore.go index c371adf5b..34bd8a124 100644 --- a/pkg/checkpoint/checkpoint_restore.go +++ b/pkg/checkpoint/checkpoint_restore.go @@ -78,6 +78,18 @@ func CRImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, restoreOpt } } + if restoreOptions.IgnoreStaticIP || restoreOptions.IgnoreStaticMAC { + for net, opts := range ctrConfig.Networks { + if restoreOptions.IgnoreStaticIP { + opts.StaticIPs = nil + } + if restoreOptions.IgnoreStaticMAC { + opts.StaticMAC = nil + } + ctrConfig.Networks[net] = opts + } + } + ctrID := ctrConfig.ID newName := false diff --git a/pkg/criu/criu.go b/pkg/criu/criu.go index 2a6805979..967da0dca 100644 --- a/pkg/criu/criu.go +++ b/pkg/criu/criu.go @@ -1,7 +1,12 @@ +// +build linux + package criu import ( "github.com/checkpoint-restore/go-criu/v5" + "github.com/checkpoint-restore/go-criu/v5/rpc" + + "google.golang.org/protobuf/proto" ) // MinCriuVersion for Podman at least CRIU 3.11 is required @@ -21,3 +26,20 @@ func CheckForCriu(version int) bool { } return result } + +func MemTrack() bool { + features, err := criu.MakeCriu().FeatureCheck( + &rpc.CriuFeatures{ + MemTrack: proto.Bool(true), + }, + ) + if err != nil { + return false + } + + if features == nil || features.MemTrack == nil { + return false + } + + return *features.MemTrack +} diff --git a/pkg/criu/criu_unsupported.go b/pkg/criu/criu_unsupported.go new file mode 100644 index 000000000..51cd0c1fd --- /dev/null +++ b/pkg/criu/criu_unsupported.go @@ -0,0 +1,7 @@ +// +build !linux + +package criu + +func MemTrack() bool { + return false +} diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index 1677c067f..ae441b7f3 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -341,6 +341,7 @@ type ContainerRunOptions struct { Rm bool SigProxy bool Spec *specgen.SpecGenerator + Passwd bool } // ContainerRunReport describes the results of running diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go index d72f64b5e..bec505163 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -27,7 +27,7 @@ type ImageEngine interface { ShowTrust(ctx context.Context, args []string, options ShowTrustOptions) (*ShowTrustReport, error) Shutdown(ctx context.Context) Tag(ctx context.Context, nameOrID string, tags []string, options ImageTagOptions) error - Transfer(ctx context.Context, scpOpts ImageScpOptions) error + Transfer(ctx context.Context, source ImageScpOptions, dest ImageScpOptions, parentFlags []string) error Tree(ctx context.Context, nameOrID string, options ImageTreeOptions) (*ImageTreeReport, error) Unmount(ctx context.Context, images []string, options ImageUnmountOptions) ([]*ImageUnmountReport, error) Untag(ctx context.Context, nameOrID string, tags []string, options ImageUntagOptions) error diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index 8b0fd2b85..62e7f67c8 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -311,30 +311,28 @@ type ImageSaveOptions struct { Quiet bool } -// ImageScpOptions provide options for securely copying images to podman remote +// ImageScpOptions provide options for securely copying images to and from a remote host type ImageScpOptions struct { - // SoureImageName is the image the user is providing to load on a remote machine - SourceImageName string - // Tag allows for a new image to be created under the given name - Tag string - // ToRemote specifies that we are loading to the remote host - ToRemote bool - // FromRemote specifies that we are loading from the remote host - FromRemote bool + // Remote determines if this entity is operating on a remote machine + Remote bool `json:"remote,omitempty"` + // File is the input/output file for the save and load Operation + File string `json:"file,omitempty"` + // Quiet Determines if the save and load operation will be done quietly + Quiet bool `json:"quiet,omitempty"` + // Image is the image the user is providing to save and load + Image string `json:"image,omitempty"` + // User is used in conjunction with Transfer to determine if a valid user was given to save from/load into + User string `json:"user,omitempty"` +} + +// ImageScpConnections provides the ssh related information used in remote image transfer +type ImageScpConnections struct { // Connections holds the raw string values for connections (ssh or unix) Connections []string // URI contains the ssh connection URLs to be used by the client URI []*url.URL - // Iden contains ssh identity keys to be used by the client - Iden []string - // Save Options used for first half of the scp operation - Save ImageSaveOptions - // Load options used for the second half of the scp operation - Load ImageLoadOptions - // Rootless determines whether we are loading locally from root storage to rootless storage - Rootless bool - // User is used in conjunction with Rootless to determine which user to use to obtain the uid - User string + // Identities contains ssh identity keys to be used by the client + Identities []string } // ImageTreeOptions provides options for ImageEngine.Tree() diff --git a/pkg/domain/entities/network.go b/pkg/domain/entities/network.go index d7389a699..34b89ae7d 100644 --- a/pkg/domain/entities/network.go +++ b/pkg/domain/entities/network.go @@ -2,6 +2,8 @@ package entities import ( "net" + + "github.com/containers/podman/v3/libpod/network/types" ) // NetworkListOptions describes options for listing networks in cli @@ -67,8 +69,8 @@ type NetworkDisconnectOptions struct { // NetworkConnectOptions describes options for connecting // a container to a network type NetworkConnectOptions struct { - Aliases []string - Container string + Container string `json:"container"` + types.PerNetworkOptions } // NetworkPruneReport containers the name of network and an error diff --git a/pkg/domain/entities/play.go b/pkg/domain/entities/play.go index ad35dfe25..39234caf8 100644 --- a/pkg/domain/entities/play.go +++ b/pkg/domain/entities/play.go @@ -26,8 +26,8 @@ type PlayKubeOptions struct { Username string // Password for authenticating against the registry. Password string - // Network - name of the CNI network to connect to. - Network string + // Networks - name of the network to connect to. + Networks []string // Quiet - suppress output when pulling images. Quiet bool // SignaturePolicy - path to a signature-policy file. diff --git a/pkg/domain/entities/pods.go b/pkg/domain/entities/pods.go index b255785c2..1b5a1be51 100644 --- a/pkg/domain/entities/pods.go +++ b/pkg/domain/entities/pods.go @@ -7,7 +7,6 @@ import ( commonFlag "github.com/containers/common/pkg/flag" "github.com/containers/podman/v3/libpod/define" - "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/pkg/specgen" "github.com/containers/podman/v3/pkg/util" "github.com/opencontainers/runtime-spec/specs-go" @@ -139,6 +138,7 @@ type PodCreateOptions struct { Userns specgen.Namespace `json:"-"` Volume []string `json:"volume,omitempty"` VolumesFrom []string `json:"volumes_from,omitempty"` + SecurityOpt []string `json:"security_opt,omitempty"` } // PodLogsOptions describes the options to extract pod logs. @@ -190,13 +190,13 @@ type ContainerCreateOptions struct { HealthTimeout string Hostname string `json:"hostname,omitempty"` HTTPProxy bool + HostUsers []string ImageVolume string Init bool InitContainerType string InitPath string Interactive bool IPC string - KernelMemory string Label []string LabelFile []string LogDriver string @@ -231,7 +231,7 @@ type ContainerCreateOptions struct { Rm bool RootFS bool Secrets []string - SecurityOpt []string + SecurityOpt []string `json:"security_opt,omitempty"` SdNotifyMode string ShmSize string SignaturePolicy string @@ -313,6 +313,7 @@ func ToPodSpecGen(s specgen.PodSpecGenerator, p *PodCreateOptions) (*specgen.Pod s.Hostname = p.Hostname s.Labels = p.Labels s.Devices = p.Devices + s.SecurityOpt = p.SecurityOpt s.NoInfra = !p.Infra if p.InfraCommand != nil && len(*p.InfraCommand) > 0 { s.InfraCommand = strings.Split(*p.InfraCommand, " ") @@ -329,11 +330,8 @@ func ToPodSpecGen(s specgen.PodSpecGenerator, p *PodCreateOptions) (*specgen.Pod if p.Net != nil { s.NetNS = p.Net.Network - s.StaticIP = p.Net.StaticIP - // type cast to types.HardwareAddr - s.StaticMAC = (*types.HardwareAddr)(p.Net.StaticMAC) s.PortMappings = p.Net.PublishPorts - s.CNINetworks = p.Net.CNINetworks + s.Networks = p.Net.Networks s.NetworkOptions = p.Net.NetworkOptions if p.Net.UseImageResolvConf { s.NoManageResolvConf = true diff --git a/pkg/domain/entities/types.go b/pkg/domain/entities/types.go index e062b9442..0348c0af5 100644 --- a/pkg/domain/entities/types.go +++ b/pkg/domain/entities/types.go @@ -45,18 +45,16 @@ type NetFlags struct { // NetOptions reflect the shared network options between // pods and containers type NetOptions struct { - AddHosts []string `json:"hostadd,omitempty"` - Aliases []string `json:"network_alias,omitempty"` - CNINetworks []string `json:"cni_networks,omitempty"` - UseImageResolvConf bool `json:"no_manage_resolv_conf,omitempty"` - DNSOptions []string `json:"dns_option,omitempty"` - DNSSearch []string `json:"dns_search,omitempty"` - DNSServers []net.IP `json:"dns_server,omitempty"` - Network specgen.Namespace `json:"netns,omitempty"` - NoHosts bool `json:"no_manage_hosts,omitempty"` - PublishPorts []types.PortMapping `json:"portmappings,omitempty"` - StaticIP *net.IP `json:"static_ip,omitempty"` - StaticMAC *net.HardwareAddr `json:"static_mac,omitempty"` + AddHosts []string `json:"hostadd,omitempty"` + Aliases []string `json:"network_alias,omitempty"` + Networks map[string]types.PerNetworkOptions `json:"networks,omitempty"` + UseImageResolvConf bool `json:"no_manage_resolv_conf,omitempty"` + DNSOptions []string `json:"dns_option,omitempty"` + DNSSearch []string `json:"dns_search,omitempty"` + DNSServers []net.IP `json:"dns_server,omitempty"` + Network specgen.Namespace `json:"netns,omitempty"` + NoHosts bool `json:"no_manage_hosts,omitempty"` + PublishPorts []types.PortMapping `json:"portmappings,omitempty"` // NetworkOptions are additional options for each network NetworkOptions map[string][]string `json:"network_options,omitempty"` } diff --git a/pkg/domain/filters/containers.go b/pkg/domain/filters/containers.go index 269cd2d27..60a1efb22 100644 --- a/pkg/domain/filters/containers.go +++ b/pkg/domain/filters/containers.go @@ -8,7 +8,6 @@ import ( "github.com/containers/podman/v3/libpod" "github.com/containers/podman/v3/libpod/define" - "github.com/containers/podman/v3/pkg/network" "github.com/containers/podman/v3/pkg/util" "github.com/pkg/errors" ) @@ -210,6 +209,15 @@ func GenerateContainerFilterFuncs(filter string, filterValues []string, r *libpo return false }, nil case "network": + var inputNetNames []string + for _, val := range filterValues { + net, err := r.Network().NetworkInspect(val) + if err != nil { + // ignore not found errors + break + } + inputNetNames = append(inputNetNames, net.Name) + } return func(c *libpod.Container) bool { networkMode := c.NetworkMode() // support docker like `--filter network=container:<IDorName>` @@ -241,18 +249,14 @@ func GenerateContainerFilterFuncs(filter string, filterValues []string, r *libpo return false } - networks, _, err := c.Networks() + networks, err := c.Networks() // if err or no networks, quick out if err != nil || len(networks) == 0 { return false } for _, net := range networks { - netID := network.GetNetworkID(net) - for _, val := range filterValues { - // match by network name or id - if val == net || val == netID { - return true - } + if util.StringInSlice(net, inputNetNames) { + return true } } return false diff --git a/pkg/domain/filters/pods.go b/pkg/domain/filters/pods.go index 9a2f0a3ba..8231dbc79 100644 --- a/pkg/domain/filters/pods.go +++ b/pkg/domain/filters/pods.go @@ -6,7 +6,6 @@ import ( "github.com/containers/podman/v3/libpod" "github.com/containers/podman/v3/libpod/define" - "github.com/containers/podman/v3/pkg/network" "github.com/containers/podman/v3/pkg/util" "github.com/pkg/errors" ) @@ -14,7 +13,7 @@ import ( // GeneratePodFilterFunc takes a filter and filtervalue (key, value) // and generates a libpod function that can be used to filter // pods -func GeneratePodFilterFunc(filter string, filterValues []string) ( +func GeneratePodFilterFunc(filter string, filterValues []string, r *libpod.Runtime) ( func(pod *libpod.Pod) bool, error) { switch filter { case "ctr-ids": @@ -128,24 +127,29 @@ func GeneratePodFilterFunc(filter string, filterValues []string) ( return false }, nil case "network": + var inputNetNames []string + for _, val := range filterValues { + net, err := r.Network().NetworkInspect(val) + if err != nil { + // ignore not found errors + break + } + inputNetNames = append(inputNetNames, net.Name) + } return func(p *libpod.Pod) bool { infra, err := p.InfraContainer() // no infra, quick out if err != nil { return false } - networks, _, err := infra.Networks() + networks, err := infra.Networks() // if err or no networks, quick out if err != nil || len(networks) == 0 { return false } for _, net := range networks { - netID := network.GetNetworkID(net) - for _, val := range filterValues { - // match by network name or id - if val == net || val == netID { - return true - } + if util.StringInSlice(net, inputNetNames) { + return true } } return false diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index d1af4a479..bf4dcff62 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -927,6 +927,7 @@ func (ic *ContainerEngine) ContainerRun(ctx context.Context, opts entities.Conta for _, w := range warn { fmt.Fprintf(os.Stderr, "%s\n", w) } + rtSpec, spec, optsN, err := generate.MakeContainer(ctx, ic.Libpod, opts.Spec) if err != nil { return nil, err diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 4346182d6..84c83ea8e 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -28,6 +28,7 @@ import ( domainUtils "github.com/containers/podman/v3/pkg/domain/utils" "github.com/containers/podman/v3/pkg/errorhandling" "github.com/containers/podman/v3/pkg/rootless" + "github.com/containers/podman/v3/utils" "github.com/containers/storage" dockerRef "github.com/docker/distribution/reference" "github.com/opencontainers/go-digest" @@ -351,65 +352,19 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri return pushError } -// Transfer moves images from root to rootless storage so the user specified in the scp call can access and use the image modified by root -func (ir *ImageEngine) Transfer(ctx context.Context, scpOpts entities.ImageScpOptions) error { - if scpOpts.User == "" { +// Transfer moves images between root and rootless storage so the user specified in the scp call can access and use the image modified by root +func (ir *ImageEngine) Transfer(ctx context.Context, source entities.ImageScpOptions, dest entities.ImageScpOptions, parentFlags []string) error { + if source.User == "" { return errors.Wrapf(define.ErrInvalidArg, "you must define a user when transferring from root to rootless storage") } - var u *user.User - scpOpts.User = strings.Split(scpOpts.User, ":")[0] // split in case provided with uid:gid - _, err := strconv.Atoi(scpOpts.User) - if err != nil { - u, err = user.Lookup(scpOpts.User) - if err != nil { - return err - } - } else { - u, err = user.LookupId(scpOpts.User) - if err != nil { - return err - } - } - uid, err := strconv.Atoi(u.Uid) - if err != nil { - return err - } - gid, err := strconv.Atoi(u.Gid) - if err != nil { - return err - } - err = os.Chown(scpOpts.Save.Output, uid, gid) // chown the output because was created by root so we need to give th euser read access - if err != nil { - return err - } - podman, err := os.Executable() if err != nil { return err } - machinectl, err := exec.LookPath("machinectl") - if err != nil { - logrus.Warn("defaulting to su since machinectl is not available, su will fail if no user session is available") - cmd := exec.Command("su", "-l", u.Username, "--command", podman+" --log-level="+logrus.GetLevel().String()+" --cgroup-manager=cgroupfs load --input="+scpOpts.Save.Output) // load the new image to the rootless storage - cmd.Stderr = os.Stderr - cmd.Stdout = os.Stdout - logrus.Debug("Executing load command su") - err = cmd.Run() - if err != nil { - return err - } - } else { - cmd := exec.Command(machinectl, "shell", "-q", u.Username+"@.host", podman, "--log-level="+logrus.GetLevel().String(), "--cgroup-manager=cgroupfs", "load", "--input", scpOpts.Save.Output) // load the new image to the rootless storage - cmd.Stderr = os.Stderr - cmd.Stdout = os.Stdout - logrus.Debug("Executing load command machinectl") - err = cmd.Run() - if err != nil { - return err - } + if rootless.IsRootless() && (len(dest.User) == 0 || dest.User == "root") { // if we are rootless and do not have a destination user we can just use sudo + return transferRootless(source, dest, podman, parentFlags) } - - return nil + return transferRootful(source, dest, podman, parentFlags) } func (ir *ImageEngine) Tag(ctx context.Context, nameOrID string, tags []string, options entities.ImageTagOptions) error { @@ -786,3 +741,123 @@ func putSignature(manifestBlob []byte, mech signature.SigningMechanism, sigStore } return nil } + +// TransferRootless creates new podman processes using exec.Command and sudo, transferring images between the given source and destination users +func transferRootless(source entities.ImageScpOptions, dest entities.ImageScpOptions, podman string, parentFlags []string) error { + var cmdSave *exec.Cmd + saveCommand := parentFlags + saveCommand = append(saveCommand, []string{"save", "--output", source.File, source.Image}...) + + loadCommand := parentFlags + loadCommand = append(loadCommand, []string{"load", "--input", dest.File}...) + + if source.User == "root" { + cmdSave = exec.Command("sudo", podman) + } else { + cmdSave = exec.Command(podman) + } + cmdSave = utils.CreateSCPCommand(cmdSave, saveCommand) + logrus.Debug("Executing save command") + err := cmdSave.Run() + if err != nil { + return err + } + + var cmdLoad *exec.Cmd + if source.User != "root" { + cmdLoad = exec.Command("sudo", podman) + } else { + cmdLoad = exec.Command(podman) + } + cmdLoad = utils.CreateSCPCommand(cmdLoad, loadCommand) + logrus.Debug("Executing load command") + err = cmdLoad.Run() + if err != nil { + return err + } + return nil +} + +// TransferRootless creates new podman processes using exec.Command and su/machinectl, transferring images between the given source and destination users +func transferRootful(source entities.ImageScpOptions, dest entities.ImageScpOptions, podman string, parentFlags []string) error { + basicCommand := []string{podman} + basicCommand = append(basicCommand, parentFlags...) + saveCommand := append(basicCommand, []string{"save", "--output", source.File, source.Image}...) + loadCommand := append(basicCommand, []string{"load", "--input", dest.File}...) + save := []string{strings.Join(saveCommand, " ")} + load := []string{strings.Join(loadCommand, " ")} + + // if executing using sudo or transferring between two users, the TransferRootless approach will not work, default to using machinectl or su as necessary. + // the approach using sudo is preferable and more straightforward. There is no reason for using sudo in these situations + // since the feature is meant to transfer from root to rootless an vice versa without explicit sudo evocaiton. + var uSave *user.User + var uLoad *user.User + var err error + source.User = strings.Split(source.User, ":")[0] // split in case provided with uid:gid + dest.User = strings.Split(dest.User, ":")[0] + uSave, err = lookupUser(source.User) + if err != nil { + return err + } + switch { + case dest.User != "": // if we are given a destination user, check that first + uLoad, err = lookupUser(dest.User) + if err != nil { + return err + } + case uSave.Name != "root": // else if we have no destination user, and source is not root that means we should be root + uLoad, err = user.LookupId("0") + if err != nil { + return err + } + default: // else if we have no dest user, and source user IS root, we want to be the default user. + uString := os.Getenv("SUDO_USER") + if uString == "" { + return errors.New("$SUDO_USER must be defined to find the default rootless user") + } + uLoad, err = user.Lookup(uString) + if err != nil { + return err + } + } + machinectl, err := exec.LookPath("machinectl") + if err != nil { + logrus.Warn("defaulting to su since machinectl is not available, su will fail if no user session is available") + err = execSu(uSave, save) + if err != nil { + return err + } + return execSu(uLoad, load) + } + err = execMachine(uSave, saveCommand, machinectl) + if err != nil { + return err + } + return execMachine(uLoad, loadCommand, machinectl) +} + +func lookupUser(u string) (*user.User, error) { + if u, err := user.LookupId(u); err == nil { + return u, nil + } + return user.Lookup(u) +} + +func execSu(execUser *user.User, command []string) error { + cmd := exec.Command("su", "-l", execUser.Username, "--command") + cmd = utils.CreateSCPCommand(cmd, command) + logrus.Debug("Executing command su") + return cmd.Run() +} + +func execMachine(execUser *user.User, command []string, machinectl string) error { + var cmd *exec.Cmd + if execUser.Uid == "0" { + cmd = exec.Command("sudo", machinectl, "shell", "-q", execUser.Username+"@.host") + } else { + cmd = exec.Command(machinectl, "shell", "-q", execUser.Username+"@.host") + } + cmd = utils.CreateSCPCommand(cmd, command) + logrus.Debug("Executing command machinectl") + return cmd.Run() +} diff --git a/pkg/domain/infra/abi/network.go b/pkg/domain/infra/abi/network.go index ee7403ed5..c7b12663c 100644 --- a/pkg/domain/infra/abi/network.go +++ b/pkg/domain/infra/abi/network.go @@ -71,7 +71,7 @@ func (ic *ContainerEngine) NetworkRm(ctx context.Context, namesOrIds []string, o } // We need to iterate containers looking to see if they belong to the given network for _, c := range containers { - networks, _, err := c.Networks() + networks, err := c.Networks() // if container vanished or network does not exist, go to next container if errors.Is(err, define.ErrNoSuchNetwork) || errors.Is(err, define.ErrNoSuchCtr) { continue @@ -124,7 +124,7 @@ func (ic *ContainerEngine) NetworkDisconnect(ctx context.Context, networkname st } func (ic *ContainerEngine) NetworkConnect(ctx context.Context, networkname string, options entities.NetworkConnectOptions) error { - return ic.Libpod.ConnectContainerToNetwork(options.Container, networkname, options.Aliases) + return ic.Libpod.ConnectContainerToNetwork(options.Container, networkname, options.PerNetworkOptions) } // NetworkExists checks if the given network exists @@ -152,7 +152,7 @@ func (ic *ContainerEngine) NetworkPrune(ctx context.Context, options entities.Ne // containers want networksToKeep := make(map[string]bool) for _, c := range cons { - nets, _, err := c.Networks() + nets, err := c.Networks() if err != nil { return nil, err } diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go index 4c024a3d8..6b3b04a0b 100644 --- a/pkg/domain/infra/abi/play.go +++ b/pkg/domain/infra/abi/play.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "io/ioutil" - "net" "os" "path/filepath" "strconv" @@ -18,6 +17,7 @@ import ( "github.com/containers/image/v5/types" "github.com/containers/podman/v3/libpod" "github.com/containers/podman/v3/libpod/define" + nettypes "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/pkg/autoupdate" "github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/specgen" @@ -190,39 +190,52 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY } } - podOpt := entities.PodCreateOptions{Infra: true, Net: &entities.NetOptions{StaticIP: &net.IP{}, StaticMAC: &net.HardwareAddr{}, NoHosts: options.NoHosts}} + podOpt := entities.PodCreateOptions{Infra: true, Net: &entities.NetOptions{NoHosts: options.NoHosts}} podOpt, err = kube.ToPodOpt(ctx, podName, podOpt, podYAML) if err != nil { return nil, err } - if options.Network != "" { - ns, cniNets, netOpts, err := specgen.ParseNetworkString(options.Network) - if err != nil { - return nil, err - } - - if (ns.IsBridge() && len(cniNets) == 0) || ns.IsHost() { - return nil, errors.Errorf("invalid value passed to --network: bridge or host networking must be configured in YAML") - } + ns, networks, netOpts, err := specgen.ParseNetworkFlag(options.Networks) + if err != nil { + return nil, err + } - podOpt.Net.Network = ns - if len(cniNets) > 0 { - podOpt.Net.CNINetworks = append(podOpt.Net.CNINetworks, cniNets...) - } - if len(netOpts) > 0 { - podOpt.Net.NetworkOptions = netOpts - } + if (ns.IsBridge() && len(networks) == 0) || ns.IsHost() { + return nil, errors.Errorf("invalid value passed to --network: bridge or host networking must be configured in YAML") } + podOpt.Net.Network = ns + podOpt.Net.Networks = networks + podOpt.Net.NetworkOptions = netOpts + + // FIXME This is very hard to support properly with a good ux if len(options.StaticIPs) > *ipIndex { - podOpt.Net.StaticIP = &options.StaticIPs[*ipIndex] + if !podOpt.Net.Network.IsBridge() { + errors.Wrap(define.ErrInvalidArg, "static ip addresses can only be set when the network mode is bridge") + } + if len(podOpt.Net.Networks) != 1 { + return nil, errors.Wrap(define.ErrInvalidArg, "cannot set static ip addresses for more than network, use netname:ip=<ip> syntax to specify ips for more than network") + } + for name, netOpts := range podOpt.Net.Networks { + netOpts.StaticIPs = append(netOpts.StaticIPs, options.StaticIPs[*ipIndex]) + podOpt.Net.Networks[name] = netOpts + } } else if len(options.StaticIPs) > 0 { // only warn if the user has set at least one ip logrus.Warn("No more static ips left using a random one") } if len(options.StaticMACs) > *ipIndex { - podOpt.Net.StaticMAC = &options.StaticMACs[*ipIndex] + if !podOpt.Net.Network.IsBridge() { + errors.Wrap(define.ErrInvalidArg, "static mac address can only be set when the network mode is bridge") + } + if len(podOpt.Net.Networks) != 1 { + return nil, errors.Wrap(define.ErrInvalidArg, "cannot set static mac address for more than network, use netname:mac=<mac> syntax to specify mac for more than network") + } + for name, netOpts := range podOpt.Net.Networks { + netOpts.StaticMAC = nettypes.HardwareAddr(options.StaticMACs[*ipIndex]) + podOpt.Net.Networks[name] = netOpts + } } else if len(options.StaticIPs) > 0 { // only warn if the user has set at least one mac logrus.Warn("No more static macs left using a random one") diff --git a/pkg/domain/infra/abi/pods.go b/pkg/domain/infra/abi/pods.go index 028de9e81..7bda7e994 100644 --- a/pkg/domain/infra/abi/pods.go +++ b/pkg/domain/infra/abi/pods.go @@ -325,7 +325,7 @@ func (ic *ContainerEngine) PodPs(ctx context.Context, options entities.PodPSOpti filters := make([]libpod.PodFilter, 0, len(options.Filters)) for k, v := range options.Filters { - f, err := dfilters.GeneratePodFilterFunc(k, v) + f, err := dfilters.GeneratePodFilterFunc(k, v, ic.Libpod) if err != nil { return nil, err } @@ -376,7 +376,7 @@ func (ic *ContainerEngine) PodPs(ctx context.Context, options entities.PodPSOpti if err != nil { return nil, err } - networks, _, err = infra.Networks() + networks, err = infra.Networks() if err != nil { return nil, err } diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index 2feb9d7ad..f26a489e6 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -123,7 +123,7 @@ func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, opts entities. return &entities.ImagePullReport{Images: pulledImages}, nil } -func (ir *ImageEngine) Transfer(ctx context.Context, scpOpts entities.ImageScpOptions) error { +func (ir *ImageEngine) Transfer(ctx context.Context, source entities.ImageScpOptions, dest entities.ImageScpOptions, parentFlags []string) error { return errors.Wrapf(define.ErrNotImplemented, "cannot use the remote client to transfer images between root and rootless storage") } diff --git a/pkg/domain/infra/tunnel/network.go b/pkg/domain/infra/tunnel/network.go index 069982d30..b5050345a 100644 --- a/pkg/domain/infra/tunnel/network.go +++ b/pkg/domain/infra/tunnel/network.go @@ -81,8 +81,7 @@ func (ic *ContainerEngine) NetworkDisconnect(ctx context.Context, networkname st // NetworkConnect removes a container from a given network func (ic *ContainerEngine) NetworkConnect(ctx context.Context, networkname string, opts entities.NetworkConnectOptions) error { - options := new(network.ConnectOptions).WithAliases(opts.Aliases) - return network.Connect(ic.ClientCtx, networkname, opts.Container, options) + return network.Connect(ic.ClientCtx, networkname, opts.Container, &opts.PerNetworkOptions) } // NetworkExists checks if the given network exists diff --git a/pkg/domain/infra/tunnel/play.go b/pkg/domain/infra/tunnel/play.go index 75952ce2c..103be0cf1 100644 --- a/pkg/domain/infra/tunnel/play.go +++ b/pkg/domain/infra/tunnel/play.go @@ -11,7 +11,7 @@ import ( func (ic *ContainerEngine) PlayKube(ctx context.Context, path string, opts entities.PlayKubeOptions) (*entities.PlayKubeReport, error) { options := new(play.KubeOptions).WithAuthfile(opts.Authfile).WithUsername(opts.Username).WithPassword(opts.Password) options.WithCertDir(opts.CertDir).WithQuiet(opts.Quiet).WithSignaturePolicy(opts.SignaturePolicy).WithConfigMaps(opts.ConfigMaps) - options.WithLogDriver(opts.LogDriver).WithNetwork(opts.Network).WithSeccompProfileRoot(opts.SeccompProfileRoot) + options.WithLogDriver(opts.LogDriver).WithNetwork(opts.Networks).WithSeccompProfileRoot(opts.SeccompProfileRoot) options.WithStaticIPs(opts.StaticIPs).WithStaticMACs(opts.StaticMACs) if len(opts.LogOptions) > 0 { options.WithLogOptions(opts.LogOptions) diff --git a/pkg/machine/config.go b/pkg/machine/config.go index 55d5dd7b4..4f2947ac0 100644 --- a/pkg/machine/config.go +++ b/pkg/machine/config.go @@ -1,4 +1,4 @@ -// +build amd64,!windows arm64,!windows +// +build amd64 arm64 package machine @@ -21,8 +21,18 @@ type InitOptions struct { IsDefault bool Memory uint64 Name string + TimeZone string URI url.URL Username string + ReExec bool +} + +type Provider interface { + NewMachine(opts InitOptions) (VM, error) + LoadVMByName(name string) (VM, error) + List(opts ListOptions) ([]*ListResponse, error) + IsValidVMName(name string) (bool, error) + CheckExclusiveActiveVM() (bool, string, error) } type RemoteConnectionType string @@ -48,6 +58,7 @@ type Download struct { Sha256sum string URL *url.URL VMName string + Size int64 } type ListOptions struct{} @@ -80,7 +91,7 @@ type RemoveOptions struct { } type VM interface { - Init(opts InitOptions) error + Init(opts InitOptions) (bool, error) Remove(name string, opts RemoveOptions) (string, func() error, error) SSH(name string, opts SSHOptions) error Start(name string, opts StartOptions) error @@ -88,7 +99,7 @@ type VM interface { } type DistributionDownload interface { - DownloadImage() error + HasUsableCache() (bool, error) Get() *Download } diff --git a/pkg/machine/connection.go b/pkg/machine/connection.go index ed1093264..d28ffcef1 100644 --- a/pkg/machine/connection.go +++ b/pkg/machine/connection.go @@ -1,4 +1,5 @@ -// +build amd64,!windows arm64,!windows +//go:build amd64 || arm64 +// +build amd64 arm64 package machine diff --git a/pkg/machine/fcos.go b/pkg/machine/fcos.go index 99197ac0e..60ab471ee 100644 --- a/pkg/machine/fcos.go +++ b/pkg/machine/fcos.go @@ -1,4 +1,4 @@ -// +build amd64,!windows arm64,!windows +// +build amd64 arm64 package machine @@ -65,25 +65,6 @@ func NewFcosDownloader(vmType, vmName, imageStream string) (DistributionDownload return fcd, nil } -func (f FcosDownload) getLocalUncompressedName() string { - uncompressedFilename := filepath.Join(filepath.Dir(f.LocalPath), f.VMName+"_"+f.ImageName) - return strings.TrimSuffix(uncompressedFilename, ".xz") -} - -func (f FcosDownload) DownloadImage() error { - // check if the latest image is already present - ok, err := UpdateAvailable(&f.Download) - if err != nil { - return err - } - if !ok { - if err := DownloadVMImage(f.URL, f.LocalPath); err != nil { - return err - } - } - return Decompress(f.LocalPath, f.getLocalUncompressedName()) -} - func (f FcosDownload) Get() *Download { return &f.Download } @@ -95,14 +76,14 @@ type fcosDownloadInfo struct { Sha256Sum string } -func UpdateAvailable(d *Download) (bool, error) { +func (f FcosDownload) HasUsableCache() (bool, error) { // check the sha of the local image if it exists // get the sha of the remote image // == dont bother to pull - if _, err := os.Stat(d.LocalPath); os.IsNotExist(err) { + if _, err := os.Stat(f.LocalPath); os.IsNotExist(err) { return false, nil } - fd, err := os.Open(d.LocalPath) + fd, err := os.Open(f.LocalPath) if err != nil { return false, err } @@ -115,7 +96,7 @@ func UpdateAvailable(d *Download) (bool, error) { if err != nil { return false, err } - return sum.Encoded() == d.Sha256sum, nil + return sum.Encoded() == f.Sha256sum, nil } func getFcosArch() string { diff --git a/pkg/machine/fedora.go b/pkg/machine/fedora.go new file mode 100644 index 000000000..cd713dde7 --- /dev/null +++ b/pkg/machine/fedora.go @@ -0,0 +1,122 @@ +// +build amd64 arm64 + +package machine + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "path/filepath" + "regexp" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +const ( + githubURL = "http://github.com/fedora-cloud/docker-brew-fedora/" +) + +type FedoraDownload struct { + Download +} + +func NewFedoraDownloader(vmType, vmName, releaseStream string) (DistributionDownload, error) { + imageName, downloadURL, size, err := getFedoraDownload(releaseStream) + if err != nil { + return nil, err + } + + dataDir, err := GetDataDir(vmType) + if err != nil { + return nil, err + } + + f := FedoraDownload{ + Download: Download{ + Arch: getFcosArch(), + Artifact: artifact, + Format: Format, + ImageName: imageName, + LocalPath: filepath.Join(dataDir, imageName), + URL: downloadURL, + VMName: vmName, + Size: size, + }, + } + f.Download.LocalUncompressedFile = f.getLocalUncompressedName() + return f, nil +} + +func (f FedoraDownload) Get() *Download { + return &f.Download +} + +func (f FedoraDownload) HasUsableCache() (bool, error) { + info, err := os.Stat(f.LocalPath) + if err != nil { + return false, nil + } + return info.Size() == f.Size, nil +} + +func truncRead(url string) ([]byte, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + + defer func() { + if err := resp.Body.Close(); err != nil { + logrus.Error(err) + } + }() + + body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 10*1024*1024)) + if err != nil { + return nil, err + } + + _, _ = io.Copy(io.Discard, resp.Body) + + return body, nil +} + +func getFedoraDownload(releaseStream string) (string, *url.URL, int64, error) { + dirURL := githubURL + "tree/" + releaseStream + "/" + getFcosArch() + "/" + body, err := truncRead(dirURL) + if err != nil { + return "", nil, -1, err + } + + rx, err := regexp.Compile(`fedora[^\"]+xz`) + if err != nil { + return "", nil, -1, err + } + file := rx.FindString(string(body)) + if len(file) <= 0 { + return "", nil, -1, fmt.Errorf("could not locate Fedora download at %s", dirURL) + } + + rawURL := githubURL + "raw/" + releaseStream + "/" + getFcosArch() + "/" + newLocation := rawURL + file + downloadURL, err := url.Parse(newLocation) + if err != nil { + return "", nil, -1, errors.Wrapf(err, "invalid URL generated from discovered Fedora file: %s", newLocation) + } + + resp, err := http.Head(newLocation) + if err != nil { + return "", nil, -1, errors.Wrapf(err, "head request failed: %s", newLocation) + } + _ = resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", nil, -1, fmt.Errorf("head request failed [%d] on download: %s", resp.StatusCode, newLocation) + } + + return file, downloadURL, resp.ContentLength, nil +} diff --git a/pkg/machine/ignition.go b/pkg/machine/ignition.go index 5c465d37d..84d3be296 100644 --- a/pkg/machine/ignition.go +++ b/pkg/machine/ignition.go @@ -1,4 +1,4 @@ -// +build amd64,!windows arm64,!windows +// +build amd64 arm64 package machine @@ -7,6 +7,10 @@ import ( "fmt" "io/ioutil" "net/url" + "os" + "path/filepath" + + "github.com/sirupsen/logrus" ) /* @@ -44,6 +48,7 @@ func getNodeGrp(grpName string) NodeGroup { type DynamicIgnition struct { Name string Key string + TimeZone string VMName string WritePath string } @@ -76,6 +81,37 @@ func NewIgnitionFile(ign DynamicIgnition) error { Links: getLinks(ign.Name), } + // Add or set the time zone for the machine + if len(ign.TimeZone) > 0 { + var ( + err error + tz string + ) + // local means the same as the host + // lookup where it is pointing to on the host + if ign.TimeZone == "local" { + tz, err = getLocalTimeZone() + if err != nil { + return err + } + } else { + tz = ign.TimeZone + } + tzLink := Link{ + Node: Node{ + Group: getNodeGrp("root"), + Path: "/etc/localtime", + Overwrite: boolToPtr(false), + User: getNodeUsr("root"), + }, + LinkEmbedded1: LinkEmbedded1{ + Hard: boolToPtr(false), + Target: filepath.Join("/usr/share/zoneinfo", tz), + }, + } + ignStorage.Links = append(ignStorage.Links, tzLink) + } + // ready is a unit file that sets up the virtual serial device // where when the VM is done configuring, it will send an ack // so a listening host knows it can being interacting with it @@ -322,6 +358,56 @@ machine_enabled=true }, }) + // get certs for current user + userHome, err := os.UserHomeDir() + if err != nil { + logrus.Warnf("Unable to copy certs via ignition %s", err.Error()) + return files + } + + certFiles := getCerts(filepath.Join(userHome, ".config/containers/certs.d")) + files = append(files, certFiles...) + + certFiles = getCerts(filepath.Join(userHome, ".config/docker/certs.d")) + files = append(files, certFiles...) + + return files +} + +func getCerts(certsDir string) []File { + var ( + files []File + ) + + certs, err := ioutil.ReadDir(certsDir) + if err == nil { + for _, cert := range certs { + b, err := ioutil.ReadFile(filepath.Join(certsDir, cert.Name())) + if err != nil { + logrus.Warnf("Unable to read cert file %s", err.Error()) + continue + } + files = append(files, File{ + Node: Node{ + Group: getNodeGrp("root"), + Path: filepath.Join("/etc/containers/certs.d/", cert.Name()), + User: getNodeUsr("root"), + }, + FileEmbedded1: FileEmbedded1{ + Append: nil, + Contents: Resource{ + Source: encodeDataURLPtr(string(b)), + }, + Mode: intToPtr(0644), + }, + }) + } + } else { + if !os.IsNotExist(err) { + logrus.Warnf("Unable to copy certs via ignition, error while reading certs from %s: %s", certsDir, err.Error()) + } + } + return files } diff --git a/pkg/machine/ignition_darwin.go b/pkg/machine/ignition_darwin.go new file mode 100644 index 000000000..9ede4b026 --- /dev/null +++ b/pkg/machine/ignition_darwin.go @@ -0,0 +1,16 @@ +//+build darwin + +package machine + +import ( + "os" + "strings" +) + +func getLocalTimeZone() (string, error) { + tzPath, err := os.Readlink("/etc/localtime") + if err != nil { + return "", err + } + return strings.TrimPrefix(tzPath, "/var/db/timezone/zoneinfo"), nil +} diff --git a/pkg/machine/ignition_linux.go b/pkg/machine/ignition_linux.go new file mode 100644 index 000000000..6db5a8e7a --- /dev/null +++ b/pkg/machine/ignition_linux.go @@ -0,0 +1,15 @@ +package machine + +import ( + "os/exec" + "strings" +) + +func getLocalTimeZone() (string, error) { + output, err := exec.Command("timedatectl", "show", "--property=Timezone").Output() + if err != nil { + return "", err + } + // Remove prepended field and the newline + return strings.TrimPrefix(strings.TrimSuffix(string(output), "\n"), "Timezone="), nil +} diff --git a/pkg/machine/ignition_schema.go b/pkg/machine/ignition_schema.go index aa4b8e060..8cfb0d04e 100644 --- a/pkg/machine/ignition_schema.go +++ b/pkg/machine/ignition_schema.go @@ -1,4 +1,4 @@ -// +build amd64,!windows arm64,!windows +// +build amd64 arm64 package machine diff --git a/pkg/machine/ignition_windows.go b/pkg/machine/ignition_windows.go new file mode 100644 index 000000000..c0de48bd3 --- /dev/null +++ b/pkg/machine/ignition_windows.go @@ -0,0 +1,7 @@ +//+build windows + +package machine + +func getLocalTimeZone() (string, error) { + return "", nil +} diff --git a/pkg/machine/keys.go b/pkg/machine/keys.go index 319fc2b4e..711b091f0 100644 --- a/pkg/machine/keys.go +++ b/pkg/machine/keys.go @@ -1,13 +1,21 @@ -// +build amd64,!windows arm64,!windows +// +build amd64 arm64 package machine import ( + "errors" + "fmt" "io/ioutil" + "os" "os/exec" + "path/filepath" "strings" + + "github.com/sirupsen/logrus" ) +var sshCommand = []string{"ssh-keygen", "-N", "", "-t", "ed25519", "-f"} + // CreateSSHKeys makes a priv and pub ssh key for interacting // the a VM. func CreateSSHKeys(writeLocation string) (string, error) { @@ -21,7 +29,42 @@ func CreateSSHKeys(writeLocation string) (string, error) { return strings.TrimSuffix(string(b), "\n"), nil } +func CreateSSHKeysPrefix(dir string, file string, passThru bool, skipExisting bool, prefix ...string) (string, error) { + location := filepath.Join(dir, file) + + _, e := os.Stat(location) + if !skipExisting || errors.Is(e, os.ErrNotExist) { + if err := generatekeysPrefix(dir, file, passThru, prefix...); err != nil { + return "", err + } + } else { + fmt.Println("Keys already exist, reusing") + } + b, err := ioutil.ReadFile(filepath.Join(dir, file) + ".pub") + if err != nil { + return "", err + } + return strings.TrimSuffix(string(b), "\n"), nil +} + // generatekeys creates an ed25519 set of keys func generatekeys(writeLocation string) error { - return exec.Command("ssh-keygen", "-N", "", "-t", "ed25519", "-f", writeLocation).Run() + args := append(append([]string{}, sshCommand[1:]...), writeLocation) + return exec.Command(sshCommand[0], args...).Run() +} + +// generatekeys creates an ed25519 set of keys +func generatekeysPrefix(dir string, file string, passThru bool, prefix ...string) error { + args := append([]string{}, prefix[1:]...) + args = append(args, sshCommand...) + args = append(args, file) + cmd := exec.Command(prefix[0], args...) + cmd.Dir = dir + if passThru { + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } + logrus.Debugf("Running wsl cmd %v in dir: %s", args, dir) + return cmd.Run() } diff --git a/pkg/machine/machine_unsupported.go b/pkg/machine/machine_unsupported.go index 9309d16bc..da1437984 100644 --- a/pkg/machine/machine_unsupported.go +++ b/pkg/machine/machine_unsupported.go @@ -1,3 +1,3 @@ -// +build !amd64 amd64,windows +// +build !amd64,!arm64 package machine diff --git a/pkg/machine/pull.go b/pkg/machine/pull.go index 3c8422a30..280b47f96 100644 --- a/pkg/machine/pull.go +++ b/pkg/machine/pull.go @@ -1,8 +1,9 @@ -// +build amd64,!windows arm64,!windows +// +build amd64 arm64 package machine import ( + "bufio" "fmt" "io" "io/ioutil" @@ -17,6 +18,7 @@ import ( "github.com/containers/image/v5/pkg/compression" "github.com/containers/storage/pkg/archive" "github.com/sirupsen/logrus" + "github.com/ulikunitz/xz" "github.com/vbauerster/mpb/v6" "github.com/vbauerster/mpb/v6/decor" ) @@ -43,7 +45,7 @@ func NewGenericDownloader(vmType, vmName, pullPath string) (DistributionDownload return nil, err } if len(getURL.Scheme) > 0 { - urlSplit := strings.Split(pullPath, "/") + urlSplit := strings.Split(getURL.Path, "/") imageName = urlSplit[len(urlSplit)-1] dl.LocalUncompressedFile = filepath.Join(dataDir, imageName) dl.URL = getURL @@ -63,39 +65,48 @@ func NewGenericDownloader(vmType, vmName, pullPath string) (DistributionDownload return gd, nil } -func (g GenericDownload) getLocalUncompressedName() string { +func (d Download) getLocalUncompressedName() string { var ( extension string ) switch { - case strings.HasSuffix(g.LocalPath, ".bz2"): + case strings.HasSuffix(d.LocalPath, ".bz2"): extension = ".bz2" - case strings.HasSuffix(g.LocalPath, ".gz"): + case strings.HasSuffix(d.LocalPath, ".gz"): extension = ".gz" - case strings.HasSuffix(g.LocalPath, ".xz"): + case strings.HasSuffix(d.LocalPath, ".xz"): extension = ".xz" } - uncompressedFilename := filepath.Join(filepath.Dir(g.LocalUncompressedFile), g.VMName+"_"+g.ImageName) + uncompressedFilename := filepath.Join(filepath.Dir(d.LocalPath), d.VMName+"_"+d.ImageName) return strings.TrimSuffix(uncompressedFilename, extension) } -func (g GenericDownload) DownloadImage() error { +func (g GenericDownload) Get() *Download { + return &g.Download +} + +func (g GenericDownload) HasUsableCache() (bool, error) { // If we have a URL for this "downloader", we now pull it - if g.URL != nil { - if err := DownloadVMImage(g.URL, g.LocalPath); err != nil { + return g.URL == nil, nil +} + +func DownloadImage(d DistributionDownload) error { + // check if the latest image is already present + ok, err := d.HasUsableCache() + if err != nil { + return err + } + if !ok { + if err := DownloadVMImage(d.Get().URL, d.Get().LocalPath); err != nil { return err } } - return Decompress(g.LocalPath, g.getLocalUncompressedName()) -} - -func (g GenericDownload) Get() *Download { - return &g.Download + return Decompress(d.Get().LocalPath, d.Get().getLocalUncompressedName()) } // DownloadVMImage downloads a VM image from url to given path // with download status -func DownloadVMImage(downloadURL fmt.Stringer, localImagePath string) error { +func DownloadVMImage(downloadURL *url2.URL, localImagePath string) error { out, err := os.Create(localImagePath) if err != nil { return err @@ -120,7 +131,7 @@ func DownloadVMImage(downloadURL fmt.Stringer, localImagePath string) error { return fmt.Errorf("error downloading VM image %s: %s", downloadURL, resp.Status) } size := resp.ContentLength - urlSplit := strings.Split(downloadURL.String(), "/") + urlSplit := strings.Split(downloadURL.Path, "/") prefix := "Downloading VM image: " + urlSplit[len(urlSplit)-1] onComplete := prefix + ": done" @@ -177,24 +188,50 @@ func Decompress(localPath, uncompressedPath string) error { // Will error out if file without .xz already exists // Maybe extracting then renameing is a good idea here.. // depends on xz: not pre-installed on mac, so it becomes a brew dependency -func decompressXZ(src string, output io.Writer) error { - cmd := exec.Command("xzcat", "-k", src) - //cmd := exec.Command("xz", "-d", "-k", "-v", src) - stdOut, err := cmd.StdoutPipe() - if err != nil { - return err +func decompressXZ(src string, output io.WriteCloser) error { + var read io.Reader + var cmd *exec.Cmd + // Prefer xz utils for fastest performance, fallback to go xi2 impl + if _, err := exec.LookPath("xzcat"); err == nil { + cmd = exec.Command("xzcat", "-k", src) + read, err = cmd.StdoutPipe() + if err != nil { + return err + } + cmd.Stderr = os.Stderr + } else { + file, err := os.Open(src) + if err != nil { + return err + } + defer file.Close() + // This XZ implementation is reliant on buffering. It is also 3x+ slower than XZ utils. + // Consider replacing with a faster implementation (e.g. xi2) if podman machine is + // updated with a larger image for the distribution base. + buf := bufio.NewReader(file) + read, err = xz.NewReader(buf) + if err != nil { + return err + } } - //cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr + + done := make(chan bool) go func() { - if _, err := io.Copy(output, stdOut); err != nil { + if _, err := io.Copy(output, read); err != nil { logrus.Error(err) } + output.Close() + done <- true }() - return cmd.Run() + + if cmd != nil { + return cmd.Run() + } + <-done + return nil } -func decompressEverythingElse(src string, output io.Writer) error { +func decompressEverythingElse(src string, output io.WriteCloser) error { f, err := os.Open(src) if err != nil { return err @@ -207,6 +244,9 @@ func decompressEverythingElse(src string, output io.Writer) error { if err := uncompressStream.Close(); err != nil { logrus.Error(err) } + if err := output.Close(); err != nil { + logrus.Error(err) + } }() _, err = io.Copy(output, uncompressStream) diff --git a/pkg/machine/qemu/config.go b/pkg/machine/qemu/config.go index c04773450..8404079a2 100644 --- a/pkg/machine/qemu/config.go +++ b/pkg/machine/qemu/config.go @@ -4,6 +4,8 @@ package qemu import "time" +type Provider struct{} + type MachineVM struct { // CPUs to be assigned to the VM CPUs uint64 @@ -44,6 +46,4 @@ var ( // defaultQMPTimeout is the timeout duration for the // qmp monitor interactions defaultQMPTimeout time.Duration = 2 * time.Second - // defaultRemoteUser describes the ssh username default - defaultRemoteUser = "core" ) diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go index 57c32bf74..a80a11573 100644 --- a/pkg/machine/qemu/machine.go +++ b/pkg/machine/qemu/machine.go @@ -21,18 +21,24 @@ import ( "github.com/containers/podman/v3/utils" "github.com/containers/storage/pkg/homedir" "github.com/digitalocean/go-qemu/qmp" + "github.com/docker/go-units" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) var ( + qemuProvider = &Provider{} // vmtype refers to qemu (vs libvirt, krun, etc) vmtype = "qemu" ) +func GetQemuProvider() machine.Provider { + return qemuProvider +} + // NewMachine initializes an instance of a virtual machine based on the qemu // virtualization. -func NewMachine(opts machine.InitOptions) (machine.VM, error) { +func (p *Provider) NewMachine(opts machine.InitOptions) (machine.VM, error) { vmConfigDir, err := machine.GetConfDir(vmtype) if err != nil { return nil, err @@ -44,16 +50,8 @@ func NewMachine(opts machine.InitOptions) (machine.VM, error) { ignitionFile := filepath.Join(vmConfigDir, vm.Name+".ign") vm.IgnitionFilePath = ignitionFile - // An image was specified - if len(opts.ImagePath) > 0 { - vm.ImagePath = opts.ImagePath - } - - // Assign remote user name. if not provided, use default + vm.ImagePath = opts.ImagePath vm.RemoteUsername = opts.Username - if len(vm.RemoteUsername) < 1 { - vm.RemoteUsername = defaultRemoteUser - } // Add a random port for ssh port, err := utils.GetRandomPort() @@ -106,7 +104,7 @@ func NewMachine(opts machine.InitOptions) (machine.VM, error) { // LoadByName reads a json file that describes a known qemu vm // and returns a vm instance -func LoadVMByName(name string) (machine.VM, error) { +func (p *Provider) LoadVMByName(name string) (machine.VM, error) { vm := new(MachineVM) vmConfigDir, err := machine.GetConfDir(vmtype) if err != nil { @@ -126,7 +124,7 @@ func LoadVMByName(name string) (machine.VM, error) { // Init writes the json configuration file to the filesystem for // other verbs (start, stop) -func (v *MachineVM) Init(opts machine.InitOptions) error { +func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { var ( key string ) @@ -135,7 +133,7 @@ func (v *MachineVM) Init(opts machine.InitOptions) error { // its existence vmConfigDir, err := machine.GetConfDir(vmtype) if err != nil { - return err + return false, err } jsonFile := filepath.Join(vmConfigDir, v.Name) + ".json" v.IdentityPath = filepath.Join(sshDir, v.Name) @@ -145,12 +143,13 @@ func (v *MachineVM) Init(opts machine.InitOptions) error { // Get image as usual v.ImageStream = opts.ImagePath dd, err := machine.NewFcosDownloader(vmtype, v.Name, opts.ImagePath) + if err != nil { - return err + return false, err } v.ImagePath = dd.Get().LocalUncompressedFile - if err := dd.DownloadImage(); err != nil { - return err + if err := machine.DownloadImage(dd); err != nil { + return false, err } default: // The user has provided an alternate image which can be a file path @@ -158,11 +157,11 @@ func (v *MachineVM) Init(opts machine.InitOptions) error { v.ImageStream = "custom" g, err := machine.NewGenericDownloader(vmtype, v.Name, opts.ImagePath) if err != nil { - return err + return false, err } v.ImagePath = g.Get().LocalUncompressedFile - if err := g.DownloadImage(); err != nil { - return err + if err := machine.DownloadImage(g); err != nil { + return false, err } } // Add arch specific options including image location @@ -174,12 +173,12 @@ func (v *MachineVM) Init(opts machine.InitOptions) error { if len(opts.IgnitionPath) < 1 { uri := machine.SSHRemoteConnection.MakeSSHURL("localhost", "/run/user/1000/podman/podman.sock", strconv.Itoa(v.Port), v.RemoteUsername) if err := machine.AddConnection(&uri, v.Name, filepath.Join(sshDir, v.Name), opts.IsDefault); err != nil { - return err + return false, err } uriRoot := machine.SSHRemoteConnection.MakeSSHURL("localhost", "/run/podman/podman.sock", strconv.Itoa(v.Port), "root") if err := machine.AddConnection(&uriRoot, v.Name+"-root", filepath.Join(sshDir, v.Name), opts.IsDefault); err != nil { - return err + return false, err } } else { fmt.Println("An ignition path was provided. No SSH connection was added to Podman") @@ -187,10 +186,10 @@ func (v *MachineVM) Init(opts machine.InitOptions) error { // Write the JSON file b, err := json.MarshalIndent(v, "", " ") if err != nil { - return err + return false, err } if err := ioutil.WriteFile(jsonFile, b, 0644); err != nil { - return err + return false, err } // User has provided ignition file so keygen @@ -198,17 +197,17 @@ func (v *MachineVM) Init(opts machine.InitOptions) error { if len(opts.IgnitionPath) < 1 { key, err = machine.CreateSSHKeys(v.IdentityPath) if err != nil { - return err + return false, err } } // Run arch specific things that need to be done if err := v.prepare(); err != nil { - return err + return false, err } originalDiskSize, err := getDiskSize(v.ImagePath) if err != nil { - return err + return false, err } // Resize the disk image to input disk size // only if the virtualdisk size is less than @@ -218,7 +217,7 @@ func (v *MachineVM) Init(opts machine.InitOptions) error { resize.Stdout = os.Stdout resize.Stderr = os.Stderr if err := resize.Run(); err != nil { - return errors.Errorf("error resizing image: %q", err) + return false, errors.Errorf("error resizing image: %q", err) } } // If the user provides an ignition file, we need to @@ -226,18 +225,20 @@ func (v *MachineVM) Init(opts machine.InitOptions) error { if len(opts.IgnitionPath) > 0 { inputIgnition, err := ioutil.ReadFile(opts.IgnitionPath) if err != nil { - return err + return false, err } - return ioutil.WriteFile(v.IgnitionFilePath, inputIgnition, 0644) + return false, ioutil.WriteFile(v.IgnitionFilePath, inputIgnition, 0644) } // Write the ignition file ign := machine.DynamicIgnition{ Name: opts.Username, Key: key, VMName: v.Name, + TimeZone: opts.TimeZone, WritePath: v.IgnitionFilePath, } - return machine.NewIgnitionFile(ign) + err = machine.NewIgnitionFile(ign) + return err == nil, err } // Start executes the qemu command line and forks it @@ -569,7 +570,7 @@ func getDiskSize(path string) (uint64, error) { } // List lists all vm's that use qemu virtualization -func List(_ machine.ListOptions) ([]*machine.ListResponse, error) { +func (p *Provider) List(_ machine.ListOptions) ([]*machine.ListResponse, error) { return GetVMInfos() } @@ -599,8 +600,8 @@ func GetVMInfos() ([]*machine.ListResponse, error) { listEntry.Stream = vm.ImageStream listEntry.VMType = "qemu" listEntry.CPUs = vm.CPUs - listEntry.Memory = vm.Memory - listEntry.DiskSize = vm.DiskSize + listEntry.Memory = vm.Memory * units.MiB + listEntry.DiskSize = vm.DiskSize * units.GiB fi, err := os.Stat(fullPath) if err != nil { return err @@ -625,7 +626,7 @@ func GetVMInfos() ([]*machine.ListResponse, error) { return listed, err } -func IsValidVMName(name string) (bool, error) { +func (p *Provider) IsValidVMName(name string) (bool, error) { infos, err := GetVMInfos() if err != nil { return false, err @@ -638,8 +639,9 @@ func IsValidVMName(name string) (bool, error) { return false, nil } -// CheckActiveVM checks if there is a VM already running -func CheckActiveVM() (bool, string, error) { +// CheckExclusiveActiveVM checks if there is a VM already running +// that does not allow other VMs to be running +func (p *Provider) CheckExclusiveActiveVM() (bool, string, error) { vms, err := GetVMInfos() if err != nil { return false, "", errors.Wrap(err, "error checking VM active") diff --git a/pkg/machine/qemu/machine_unsupported.go b/pkg/machine/qemu/machine_unsupported.go index da06ac324..e3ce05e3d 100644 --- a/pkg/machine/qemu/machine_unsupported.go +++ b/pkg/machine/qemu/machine_unsupported.go @@ -1,3 +1,3 @@ -// +build !amd64 amd64,windows +// +build !amd64,!arm64 windows package qemu diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go new file mode 100644 index 000000000..b4ee79acc --- /dev/null +++ b/pkg/machine/wsl/machine.go @@ -0,0 +1,1119 @@ +//go:build windows +// +build windows + +package wsl + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/containers/podman/v3/pkg/machine" + "github.com/containers/podman/v3/utils" + "github.com/containers/storage/pkg/homedir" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "golang.org/x/text/encoding/unicode" + "golang.org/x/text/transform" +) + +var ( + wslProvider = &Provider{} + // vmtype refers to qemu (vs libvirt, krun, etc) + vmtype = "wsl" +) + +const ( + ErrorSuccessRebootInitiated = 1641 + ErrorSuccessRebootRequired = 3010 +) + +// Usermode networking avoids potential nftables compatibility issues between the distro +// and the WSL Kernel. Additionally it avoids fw rule conflicts between distros, since +// all instances run under the same Kernel at runtime +const containersConf = `[containers] + +[engine] +cgroup_manager = "cgroupfs" +events_logger = "file" +` + +const appendPort = `grep -q Port\ %d /etc/ssh/sshd_config || echo Port %d >> /etc/ssh/sshd_config` + +const configServices = `ln -fs /usr/lib/systemd/system/sshd.service /etc/systemd/system/multi-user.target.wants/sshd.service +ln -fs /usr/lib/systemd/system/podman.socket /etc/systemd/system/sockets.target.wants/podman.socket +rm -f /etc/systemd/system/getty.target.wants/console-getty.service +rm -f /etc/systemd/system/getty.target.wants/getty@tty1.service +rm -f /etc/systemd/system/multi-user.target.wants/systemd-resolved.service +rm -f /etc/systemd/system/dbus-org.freedesktop.resolve1.service +ln -fs /dev/null /etc/systemd/system/console-getty.service +mkdir -p /etc/systemd/system/systemd-sysusers.service.d/ +adduser -m [USER] -G wheel +mkdir -p /home/[USER]/.config/systemd/[USER]/ +chown [USER]:[USER] /home/[USER]/.config +` + +const sudoers = `%wheel ALL=(ALL) NOPASSWD: ALL +` + +const bootstrap = `#!/bin/bash +ps -ef | grep -v grep | grep -q systemd && exit 0 +nohup unshare --kill-child --fork --pid --mount --mount-proc --propagation shared /lib/systemd/systemd >/dev/null 2>&1 & +sleep 0.1 +` + +const wslmotd = ` +You will be automatically entered into a nested process namespace where +systemd is running. If you need to access the parent namespace, hit ctrl-d +or type exit. This also means to log out you need to exit twice. + +` + +const sysdpid = "SYSDPID=`ps -eo cmd,pid | grep -m 1 ^/lib/systemd/systemd | awk '{print $2}'`" + +const profile = sysdpid + ` +if [ ! -z "$SYSDPID" ] && [ "$SYSDPID" != "1" ]; then + cat /etc/wslmotd + /usr/local/bin/enterns +fi +` + +const enterns = "#!/bin/bash\n" + sysdpid + ` +if [ ! -z "$SYSDPID" ] && [ "$SYSDPID" != "1" ]; then + nsenter -m -p -t $SYSDPID "$@" +fi +` + +const waitTerm = sysdpid + ` +if [ ! -z "$SYSDPID" ]; then + timeout 60 tail -f /dev/null --pid $SYSDPID +fi +` + +// WSL kernel does not have sg and crypto_user modules +const overrideSysusers = `[Service] +LoadCredential= +` + +const lingerService = `[Unit] +Description=A systemd user unit demo +After=network-online.target +Wants=network-online.target podman.socket +[Service] +ExecStart=/usr/bin/sleep infinity +` + +const lingerSetup = `mkdir -p /home/[USER]/.config/systemd/[USER]/default.target.wants +ln -fs /home/[USER]/.config/systemd/[USER]/linger-example.service \ + /home/[USER]/.config/systemd/[USER]/default.target.wants/linger-example.service +` + +const wslInstallError = `Could not %s. See previous output for any potential failure details. +If you can not resolve the issue, and rerunning fails, try the "wsl --install" process +outlined in the following article: + +http://docs.microsoft.com/en-us/windows/wsl/install + +` + +const wslKernelError = `Could not %s. See previous output for any potential failure details. +If you can not resolve the issue, try rerunning the "podman machine init command". If that fails +try the "wsl --update" command and then rerun "podman machine init". Finally, if all else fails, +try following the steps outlined in the following article: + +http://docs.microsoft.com/en-us/windows/wsl/install + +` + +const wslInstallKernel = "install the WSL Kernel" + +const wslOldVersion = `Automatic installation of WSL can not be performed on this version of Windows +Either update to Build 19041 (or later), or perform the manual installation steps +outlined in the following article: + +http://docs.microsoft.com/en-us/windows/wsl/install\ + +` + +type Provider struct{} + +type MachineVM struct { + // IdentityPath is the fq path to the ssh priv key + IdentityPath string + // IgnitionFilePath is the fq path to the .ign file + ImageStream string + // ImagePath is the fq path to + ImagePath string + // Name of the vm + Name string + // SSH port for user networking + Port int + // RemoteUsername of the vm user + RemoteUsername string +} + +type ExitCodeError struct { + code uint +} + +func (e *ExitCodeError) Error() string { + return fmt.Sprintf("Process failed with exit code: %d", e.code) +} + +func GetWSLProvider() machine.Provider { + return wslProvider +} + +// NewMachine initializes an instance of a virtual machine based on the qemu +// virtualization. +func (p *Provider) NewMachine(opts machine.InitOptions) (machine.VM, error) { + vm := new(MachineVM) + if len(opts.Name) > 0 { + vm.Name = opts.Name + } + + vm.ImagePath = opts.ImagePath + vm.RemoteUsername = opts.Username + + // Add a random port for ssh + port, err := utils.GetRandomPort() + if err != nil { + return nil, err + } + vm.Port = port + + return vm, nil +} + +// LoadByName reads a json file that describes a known qemu vm +// and returns a vm instance +func (p *Provider) LoadVMByName(name string) (machine.VM, error) { + vm := new(MachineVM) + vmConfigDir, err := machine.GetConfDir(vmtype) + if err != nil { + return nil, err + } + b, err := ioutil.ReadFile(filepath.Join(vmConfigDir, name+".json")) + if os.IsNotExist(err) { + return nil, errors.Wrap(machine.ErrNoSuchVM, name) + } + if err != nil { + return nil, err + } + err = json.Unmarshal(b, vm) + return vm, err +} + +// Init writes the json configuration file to the filesystem for +// other verbs (start, stop) +func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { + if cont, err := checkAndInstallWSL(opts); !cont { + appendOutputIfError(opts.ReExec, err) + return cont, err + } + + homeDir := homedir.Get() + sshDir := filepath.Join(homeDir, ".ssh") + v.IdentityPath = filepath.Join(sshDir, v.Name) + + if err := downloadDistro(v, opts); err != nil { + return false, err + } + + if err := writeJSON(v); err != nil { + return false, err + } + + if err := setupConnections(v, opts, sshDir); err != nil { + return false, err + } + + dist, err := provisionWSLDist(v) + if err != nil { + return false, err + } + + fmt.Println("Configuring system...") + if err = configureSystem(v, dist); err != nil { + return false, err + } + + if err = installScripts(dist); err != nil { + return false, err + } + + if err = createKeys(v, dist, sshDir); err != nil { + return false, err + } + + return true, nil +} + +func downloadDistro(v *MachineVM, opts machine.InitOptions) error { + var ( + dd machine.DistributionDownload + err error + ) + + if _, e := strconv.Atoi(opts.ImagePath); e == nil { + v.ImageStream = opts.ImagePath + dd, err = machine.NewFedoraDownloader(vmtype, v.Name, v.ImageStream) + } else { + v.ImageStream = "custom" + dd, err = machine.NewGenericDownloader(vmtype, v.Name, opts.ImagePath) + } + if err != nil { + return err + } + + v.ImagePath = dd.Get().LocalUncompressedFile + return machine.DownloadImage(dd) +} + +func writeJSON(v *MachineVM) error { + vmConfigDir, err := machine.GetConfDir(vmtype) + if err != nil { + return err + } + + jsonFile := filepath.Join(vmConfigDir, v.Name) + ".json" + + b, err := json.MarshalIndent(v, "", " ") + if err != nil { + return err + } + if err := ioutil.WriteFile(jsonFile, b, 0644); err != nil { + return errors.Wrap(err, "could not write machine json config") + } + + return nil +} + +func setupConnections(v *MachineVM, opts machine.InitOptions, sshDir string) error { + uriRoot := machine.SSHRemoteConnection.MakeSSHURL("localhost", "/run/podman/podman.sock", strconv.Itoa(v.Port), "root") + if err := machine.AddConnection(&uriRoot, v.Name+"-root", filepath.Join(sshDir, v.Name), opts.IsDefault); err != nil { + return err + } + + user := opts.Username + uri := machine.SSHRemoteConnection.MakeSSHURL("localhost", withUser("/run/[USER]/1000/podman/podman.sock", user), strconv.Itoa(v.Port), v.RemoteUsername) + return machine.AddConnection(&uri, v.Name, filepath.Join(sshDir, v.Name), opts.IsDefault) +} + +func provisionWSLDist(v *MachineVM) (string, error) { + vmDataDir, err := machine.GetDataDir(vmtype) + if err != nil { + return "", err + } + + distDir := filepath.Join(vmDataDir, "wsldist") + distTarget := filepath.Join(distDir, v.Name) + if err := os.MkdirAll(distDir, 0755); err != nil { + return "", errors.Wrap(err, "could not create wsldist directory") + } + + dist := toDist(v.Name) + fmt.Println("Importing operating system into WSL (this may take 5+ minutes on a new WSL install)...") + if err = runCmdPassThrough("wsl", "--import", dist, distTarget, v.ImagePath); err != nil { + return "", errors.Wrap(err, "WSL import of guest OS failed") + } + + fmt.Println("Installing packages (this will take awhile)...") + if err = runCmdPassThrough("wsl", "-d", dist, "dnf", "upgrade", "-y"); err != nil { + return "", errors.Wrap(err, "package upgrade on guest OS failed") + } + + if err = runCmdPassThrough("wsl", "-d", dist, "dnf", "install", + "podman", "podman-docker", "openssh-server", "procps-ng", "-y"); err != nil { + return "", errors.Wrap(err, "package installation on guest OS failed") + } + + // Fixes newuidmap + if err = runCmdPassThrough("wsl", "-d", dist, "dnf", "reinstall", "shadow-utils", "-y"); err != nil { + return "", errors.Wrap(err, "package reinstallation of shadow-utils on guest OS failed") + } + + // Windows 11 (NT Version = 10, Build 22000) generates harmless but scary messages on every + // operation when mount was not present on the initial start. Force a cycle so that it won't + // repeatedly complain. + if winVersionAtLeast(10, 0, 22000) { + if err := runCmdPassThrough("wsl", "--terminate", dist); err != nil { + logrus.Warnf("could not cycle WSL dist: %s", err.Error()) + } + } + + return dist, nil +} + +func createKeys(v *MachineVM, dist string, sshDir string) error { + user := v.RemoteUsername + + if err := os.MkdirAll(sshDir, 0700); err != nil { + return errors.Wrap(err, "could not create ssh directory") + } + + if err := runCmdPassThrough("wsl", "--terminate", dist); err != nil { + return errors.Wrap(err, "could not cycle WSL dist") + } + + key, err := machine.CreateSSHKeysPrefix(sshDir, v.Name, true, true, "wsl", "-d", dist) + if err != nil { + return errors.Wrap(err, "could not create ssh keys") + } + + if err := pipeCmdPassThrough("wsl", key+"\n", "-d", dist, "sh", "-c", "mkdir -p /root/.ssh;"+ + "cat >> /root/.ssh/authorized_keys; chmod 600 /root/.ssh/authorized_keys"); err != nil { + return errors.Wrap(err, "could not create root authorized keys on guest OS") + } + + userAuthCmd := withUser("mkdir -p /home/[USER]/.ssh;"+ + "cat >> /home/[USER]/.ssh/authorized_keys; chown -R [USER]:[USER] /home/[USER]/.ssh;"+ + "chmod 600 /home/[USER]/.ssh/authorized_keys", user) + if err := pipeCmdPassThrough("wsl", key+"\n", "-d", dist, "sh", "-c", userAuthCmd); err != nil { + return errors.Wrapf(err, "could not create '%s' authorized keys on guest OS", v.RemoteUsername) + } + + return nil +} + +func configureSystem(v *MachineVM, dist string) error { + user := v.RemoteUsername + if err := runCmdPassThrough("wsl", "-d", dist, "sh", "-c", fmt.Sprintf(appendPort, v.Port, v.Port)); err != nil { + return errors.Wrap(err, "could not configure SSH port for guest OS") + } + + if err := pipeCmdPassThrough("wsl", withUser(configServices, user), "-d", dist, "sh"); err != nil { + return errors.Wrap(err, "could not configure systemd settings for guest OS") + } + + if err := pipeCmdPassThrough("wsl", sudoers, "-d", dist, "sh", "-c", "cat >> /etc/sudoers"); err != nil { + return errors.Wrap(err, "could not add wheel to sudoers") + } + + if err := pipeCmdPassThrough("wsl", overrideSysusers, "-d", dist, "sh", "-c", + "cat > /etc/systemd/system/systemd-sysusers.service.d/override.conf"); err != nil { + return errors.Wrap(err, "could not generate systemd-sysusers override for guest OS") + } + + lingerCmd := withUser("cat > /home/[USER]/.config/systemd/[USER]/linger-example.service", user) + if err := pipeCmdPassThrough("wsl", lingerService, "-d", dist, "sh", "-c", lingerCmd); err != nil { + return errors.Wrap(err, "could not generate linger service for guest OS") + } + + if err := pipeCmdPassThrough("wsl", withUser(lingerSetup, user), "-d", dist, "sh"); err != nil { + return errors.Wrap(err, "could not configure systemd settomgs for guest OS") + } + + if err := pipeCmdPassThrough("wsl", containersConf, "-d", dist, "sh", "-c", "cat > /etc/containers/containers.conf"); err != nil { + return errors.Wrap(err, "could not create containers.conf for guest OS") + } + + return nil +} + +func installScripts(dist string) error { + if err := pipeCmdPassThrough("wsl", enterns, "-d", dist, "sh", "-c", + "cat > /usr/local/bin/enterns; chmod 755 /usr/local/bin/enterns"); err != nil { + return errors.Wrap(err, "could not create enterns script for guest OS") + } + + if err := pipeCmdPassThrough("wsl", profile, "-d", dist, "sh", "-c", + "cat > /etc/profile.d/enterns.sh"); err != nil { + return errors.Wrap(err, "could not create motd profile script for guest OS") + } + + if err := pipeCmdPassThrough("wsl", wslmotd, "-d", dist, "sh", "-c", "cat > /etc/wslmotd"); err != nil { + return errors.Wrap(err, "could not create a WSL MOTD for guest OS") + } + + if err := pipeCmdPassThrough("wsl", bootstrap, "-d", dist, "sh", "-c", + "cat > /root/bootstrap; chmod 755 /root/bootstrap"); err != nil { + return errors.Wrap(err, "could not create bootstrap script for guest OS") + } + + return nil +} + +func checkAndInstallWSL(opts machine.InitOptions) (bool, error) { + if isWSLInstalled() { + return true, nil + } + + admin := hasAdminRights() + + if !isWSLFeatureEnabled() { + return false, attemptFeatureInstall(opts, admin) + } + + skip := false + if !opts.ReExec && !admin { + fmt.Println("Launching WSL Kernel Install...") + if err := launchElevate(wslInstallKernel); err != nil { + return false, err + } + + skip = true + } + + if !skip { + if err := installWslKernel(); err != nil { + fmt.Fprintf(os.Stderr, wslKernelError, wslInstallKernel) + return false, err + } + + if opts.ReExec { + return false, nil + } + } + + return true, nil +} + +func attemptFeatureInstall(opts machine.InitOptions, admin bool) error { + if !winVersionAtLeast(10, 0, 18362) { + return errors.Errorf("Your version of Windows does not support WSL. Update to Windows 10 Build 19041 or later") + } else if !winVersionAtLeast(10, 0, 19041) { + fmt.Fprint(os.Stderr, wslOldVersion) + return errors.Errorf("WSL can not be automatically installed") + } + + message := "WSL is not installed on this system, installing it.\n\n" + + if !admin { + message += "Since you are not running as admin, a new window will open and " + + "require you to approve administrator privileges.\n\n" + } + + message += "NOTE: A system reboot will be required as part of this process. " + + "If you prefer, you may abort now, and perform a manual installation using the \"wsl --install\" command." + + if !opts.ReExec && MessageBox(message, "Podman Machine", false) != 1 { + return errors.Errorf("WSL installation aborted") + } + + if !opts.ReExec && !admin { + return launchElevate("install the Windows WSL Features") + } + + return installWsl() +} + +func launchElevate(operation string) error { + truncateElevatedOutputFile() + err := relaunchElevatedWait() + if err != nil { + if eerr, ok := err.(*ExitCodeError); ok { + if eerr.code == ErrorSuccessRebootRequired { + fmt.Println("Reboot is required to continue installation, please reboot at your convenience") + return nil + } + } + + fmt.Fprintf(os.Stderr, "Elevated process failed with error: %v\n\n", err) + dumpOutputFile() + fmt.Fprintf(os.Stderr, wslInstallError, operation) + } + return err +} + +func installWsl() error { + log, err := getElevatedOutputFileWrite() + if err != nil { + return err + } + defer log.Close() + if err := runCmdPassThroughTee(log, "dism", "/online", "/enable-feature", + "/featurename:Microsoft-Windows-Subsystem-Linux", "/all", "/norestart"); isMsiError(err) { + return errors.Wrap(err, "could not enable WSL Feature") + } + + if err = runCmdPassThroughTee(log, "dism", "/online", "/enable-feature", + "/featurename:VirtualMachinePlatform", "/all", "/norestart"); isMsiError(err) { + return errors.Wrap(err, "could not enable Virtual Machine Feature") + } + log.Close() + + return reboot() +} + +func installWslKernel() error { + log, err := getElevatedOutputFileWrite() + if err != nil { + return err + } + defer log.Close() + + message := "Installing WSL Kernel Update" + fmt.Println(message) + fmt.Fprintln(log, message) + + backoff := 500 * time.Millisecond + for i := 0; i < 5; i++ { + err = runCmdPassThroughTee(log, "wsl", "--update") + if err == nil { + break + } + // In case of unusual circumstances (e.g. race with installer actions) + // retry a few times + message = "An error occured attempting the WSL Kernel update, retrying..." + fmt.Println(message) + fmt.Fprintln(log, message) + time.Sleep(backoff) + backoff *= 2 + } + + if err != nil { + return errors.Wrap(err, "could not install WSL Kernel") + } + + return nil +} + +func getElevatedOutputFileName() (string, error) { + dir, err := homedir.GetDataHome() + if err != nil { + return "", err + } + return filepath.Join(dir, "podman-elevated-output.log"), nil +} + +func dumpOutputFile() { + file, err := getElevatedOutputFileRead() + if err != nil { + logrus.Debug("could not find elevated child output file") + return + } + defer file.Close() + _, _ = io.Copy(os.Stdout, file) +} + +func getElevatedOutputFileRead() (*os.File, error) { + return getElevatedOutputFile(os.O_RDONLY) +} + +func getElevatedOutputFileWrite() (*os.File, error) { + return getElevatedOutputFile(os.O_WRONLY | os.O_CREATE | os.O_APPEND) +} + +func appendOutputIfError(write bool, err error) { + if write && err == nil { + return + } + + if file, check := getElevatedOutputFileWrite(); check == nil { + defer file.Close() + fmt.Fprintf(file, "Error: %v\n", err) + } +} + +func truncateElevatedOutputFile() error { + name, err := getElevatedOutputFileName() + if err != nil { + return err + } + + return os.Truncate(name, 0) +} + +func getElevatedOutputFile(mode int) (*os.File, error) { + name, err := getElevatedOutputFileName() + if err != nil { + return nil, err + } + + dir, err := homedir.GetDataHome() + if err != nil { + return nil, err + } + + if err = os.MkdirAll(dir, 0755); err != nil { + return nil, err + } + + return os.OpenFile(name, mode, 0644) +} + +func isMsiError(err error) bool { + if err == nil { + return false + } + + if eerr, ok := err.(*exec.ExitError); ok { + switch eerr.ExitCode() { + case 0: + fallthrough + case ErrorSuccessRebootInitiated: + fallthrough + case ErrorSuccessRebootRequired: + return false + } + } + + return true +} +func toDist(name string) string { + if !strings.HasPrefix(name, "podman") { + name = "podman-" + name + } + return name +} + +func withUser(s string, user string) string { + return strings.ReplaceAll(s, "[USER]", user) +} + +func runCmdPassThrough(name string, arg ...string) error { + logrus.Debugf("Running command: %s %v", name, arg) + cmd := exec.Command(name, arg...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func runCmdPassThroughTee(out io.Writer, name string, arg ...string) error { + logrus.Debugf("Running command: %s %v", name, arg) + + // TODO - Perhaps improve this with a conpty pseudo console so that + // dism installer text bars mirror console behavior (redraw) + cmd := exec.Command(name, arg...) + cmd.Stdin = os.Stdin + cmd.Stdout = io.MultiWriter(os.Stdout, out) + cmd.Stderr = io.MultiWriter(os.Stderr, out) + return cmd.Run() +} + +func pipeCmdPassThrough(name string, input string, arg ...string) error { + logrus.Debugf("Running command: %s %v", name, arg) + cmd := exec.Command(name, arg...) + cmd.Stdin = strings.NewReader(input) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func (v *MachineVM) Start(name string, _ machine.StartOptions) error { + if v.isRunning() { + return errors.Errorf("%q is already running", name) + } + + fmt.Println("Starting machine...") + + dist := toDist(name) + + err := runCmdPassThrough("wsl", "-d", dist, "/root/bootstrap") + if err != nil { + return errors.Wrap(err, "WSL bootstrap script failed") + } + + return markStart(name) +} + +func isWSLInstalled() bool { + cmd := exec.Command("wsl", "--status") + out, err := cmd.StdoutPipe() + if err != nil { + return false + } + if err = cmd.Start(); err != nil { + return false + } + scanner := bufio.NewScanner(transform.NewReader(out, unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder())) + result := true + for scanner.Scan() { + line := scanner.Text() + // Windows 11 does not set an error exit code when a kernel is not avail + if strings.Contains(line, "kernel file is not found") { + result = false + break + } + } + if err := cmd.Wait(); !result || err != nil { + return false + } + + return true +} + +func isWSLFeatureEnabled() bool { + cmd := exec.Command("wsl", "--set-default-version", "2") + return cmd.Run() == nil +} + +func isWSLRunning(dist string) (bool, error) { + cmd := exec.Command("wsl", "-l", "--running") + out, err := cmd.StdoutPipe() + if err != nil { + return false, err + } + if err = cmd.Start(); err != nil { + return false, err + } + scanner := bufio.NewScanner(transform.NewReader(out, unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder())) + result := false + for scanner.Scan() { + fields := strings.Fields(scanner.Text()) + if len(fields) > 0 && dist == fields[0] { + result = true + break + } + } + + _ = cmd.Wait() + + return result, nil +} + +func isSystemdRunning(dist string) (bool, error) { + cmd := exec.Command("wsl", "-d", dist, "sh") + cmd.Stdin = strings.NewReader(sysdpid + "\necho $SYSDPID\n") + out, err := cmd.StdoutPipe() + if err != nil { + return false, err + } + if err = cmd.Start(); err != nil { + return false, err + } + scanner := bufio.NewScanner(out) + result := false + if scanner.Scan() { + text := scanner.Text() + i, err := strconv.Atoi(text) + if err == nil && i > 0 { + result = true + } + } + + _ = cmd.Wait() + + return result, nil +} + +func (v *MachineVM) Stop(name string, _ machine.StopOptions) error { + dist := toDist(v.Name) + + wsl, err := isWSLRunning(dist) + if err != nil { + return err + } + + sysd := false + if wsl { + sysd, err = isSystemdRunning(dist) + if err != nil { + return err + } + } + + if !wsl || !sysd { + return errors.Errorf("%q is not running", v.Name) + } + + cmd := exec.Command("wsl", "-d", dist, "sh") + cmd.Stdin = strings.NewReader(waitTerm) + if err = cmd.Start(); err != nil { + return errors.Wrap(err, "Error executing wait command") + } + + exitCmd := exec.Command("wsl", "-d", dist, "/usr/local/bin/enterns", "systemctl", "exit", "0") + if err = exitCmd.Run(); err != nil { + return errors.Wrap(err, "Error stopping sysd") + } + + if err = cmd.Wait(); err != nil { + return err + } + + cmd = exec.Command("wsl", "--terminate", dist) + if err = cmd.Run(); err != nil { + return err + } + + return nil +} + +//nolint:cyclop +func (v *MachineVM) Remove(name string, opts machine.RemoveOptions) (string, func() error, error) { + var files []string + + if v.isRunning() { + return "", nil, errors.Errorf("running vm %q cannot be destroyed", v.Name) + } + + // Collect all the files that need to be destroyed + if !opts.SaveKeys { + files = append(files, v.IdentityPath, v.IdentityPath+".pub") + } + if !opts.SaveImage { + files = append(files, v.ImagePath) + } + + vmConfigDir, err := machine.GetConfDir(vmtype) + if err != nil { + return "", nil, err + } + files = append(files, filepath.Join(vmConfigDir, v.Name+".json")) + + vmDataDir, err := machine.GetDataDir(vmtype) + if err != nil { + return "", nil, err + } + files = append(files, filepath.Join(vmDataDir, "wsldist", v.Name)) + + confirmationMessage := "\nThe following files will be deleted:\n\n" + for _, msg := range files { + confirmationMessage += msg + "\n" + } + + confirmationMessage += "\n" + return confirmationMessage, func() error { + if err := machine.RemoveConnection(v.Name); err != nil { + logrus.Error(err) + } + if err := machine.RemoveConnection(v.Name + "-root"); err != nil { + logrus.Error(err) + } + if err := runCmdPassThrough("wsl", "--unregister", toDist(v.Name)); err != nil { + logrus.Error(err) + } + for _, f := range files { + if err := os.RemoveAll(f); err != nil { + logrus.Error(err) + } + } + return nil + }, nil +} + +func (v *MachineVM) isRunning() bool { + dist := toDist(v.Name) + + wsl, err := isWSLRunning(dist) + if err != nil { + return false + } + + sysd := false + if wsl { + sysd, err = isSystemdRunning(dist) + + if err != nil { + return false + } + } + + return sysd +} + +// SSH opens an interactive SSH session to the vm specified. +// Added ssh function to VM interface: pkg/machine/config/go : line 58 +func (v *MachineVM) SSH(name string, opts machine.SSHOptions) error { + if !v.isRunning() { + return errors.Errorf("vm %q is not running.", v.Name) + } + + username := opts.Username + if username == "" { + username = v.RemoteUsername + } + + sshDestination := username + "@localhost" + port := strconv.Itoa(v.Port) + + args := []string{"-i", v.IdentityPath, "-p", port, sshDestination, "-o", "UserKnownHostsFile /dev/null", "-o", "StrictHostKeyChecking no"} + if len(opts.Args) > 0 { + args = append(args, opts.Args...) + } else { + fmt.Printf("Connecting to vm %s. To close connection, use `~.` or `exit`\n", v.Name) + } + + cmd := exec.Command("ssh", args...) + logrus.Debugf("Executing: ssh %v\n", args) + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + + return cmd.Run() +} + +// List lists all vm's that use qemu virtualization +func (p *Provider) List(_ machine.ListOptions) ([]*machine.ListResponse, error) { + return GetVMInfos() +} + +func GetVMInfos() ([]*machine.ListResponse, error) { + vmConfigDir, err := machine.GetConfDir(vmtype) + if err != nil { + return nil, err + } + + var listed []*machine.ListResponse + + if err = filepath.Walk(vmConfigDir, func(path string, info os.FileInfo, err error) error { + vm := new(MachineVM) + if strings.HasSuffix(info.Name(), ".json") { + fullPath := filepath.Join(vmConfigDir, info.Name()) + b, err := ioutil.ReadFile(fullPath) + if err != nil { + return err + } + err = json.Unmarshal(b, vm) + if err != nil { + return err + } + listEntry := new(machine.ListResponse) + + listEntry.Name = vm.Name + listEntry.Stream = vm.ImageStream + listEntry.VMType = "wsl" + listEntry.CPUs, _ = getCPUs(vm) + listEntry.Memory, _ = getMem(vm) + listEntry.DiskSize = getDiskSize(vm) + fi, err := os.Stat(fullPath) + if err != nil { + return err + } + listEntry.CreatedAt = fi.ModTime() + listEntry.LastUp = getLastStart(vm, fi.ModTime()) + if vm.isRunning() { + listEntry.Running = true + } + + listed = append(listed, listEntry) + } + return nil + }); err != nil { + return nil, err + } + return listed, err +} + +func getDiskSize(vm *MachineVM) uint64 { + vmDataDir, err := machine.GetDataDir(vmtype) + if err != nil { + return 0 + } + distDir := filepath.Join(vmDataDir, "wsldist") + disk := filepath.Join(distDir, vm.Name, "ext4.vhdx") + info, err := os.Stat(disk) + if err != nil { + return 0 + } + return uint64(info.Size()) +} + +func markStart(name string) error { + vmDataDir, err := machine.GetDataDir(vmtype) + if err != nil { + return err + } + distDir := filepath.Join(vmDataDir, "wsldist") + start := filepath.Join(distDir, name, "laststart") + file, err := os.Create(start) + if err != nil { + return err + } + file.Close() + + return nil +} + +func getLastStart(vm *MachineVM, created time.Time) time.Time { + vmDataDir, err := machine.GetDataDir(vmtype) + if err != nil { + return created + } + distDir := filepath.Join(vmDataDir, "wsldist") + start := filepath.Join(distDir, vm.Name, "laststart") + info, err := os.Stat(start) + if err != nil { + return created + } + return info.ModTime() +} + +func getCPUs(vm *MachineVM) (uint64, error) { + dist := toDist(vm.Name) + if run, _ := isWSLRunning(dist); !run { + return 0, nil + } + cmd := exec.Command("wsl", "-d", dist, "nproc") + out, err := cmd.StdoutPipe() + if err != nil { + return 0, err + } + if err = cmd.Start(); err != nil { + return 0, err + } + scanner := bufio.NewScanner(out) + var result string + for scanner.Scan() { + result = scanner.Text() + } + _ = cmd.Wait() + + ret, err := strconv.Atoi(result) + return uint64(ret), err +} + +func getMem(vm *MachineVM) (uint64, error) { + dist := toDist(vm.Name) + if run, _ := isWSLRunning(dist); !run { + return 0, nil + } + cmd := exec.Command("wsl", "-d", dist, "cat", "/proc/meminfo") + out, err := cmd.StdoutPipe() + if err != nil { + return 0, err + } + if err = cmd.Start(); err != nil { + return 0, err + } + scanner := bufio.NewScanner(out) + var ( + total, available uint64 + t, a int + ) + for scanner.Scan() { + fields := strings.Fields(scanner.Text()) + if strings.HasPrefix(fields[0], "MemTotal") && len(fields) >= 2 { + t, err = strconv.Atoi(fields[1]) + total = uint64(t) * 1024 + } else if strings.HasPrefix(fields[0], "MemAvailable") && len(fields) >= 2 { + a, err = strconv.Atoi(fields[1]) + available = uint64(a) * 1024 + } + if err != nil { + break + } + } + _ = cmd.Wait() + + return total - available, err +} + +func (p *Provider) IsValidVMName(name string) (bool, error) { + infos, err := GetVMInfos() + if err != nil { + return false, err + } + for _, vm := range infos { + if vm.Name == name { + return true, nil + } + } + return false, nil +} + +func (p *Provider) CheckExclusiveActiveVM() (bool, string, error) { + return false, "", nil +} diff --git a/pkg/machine/wsl/machine_unsupported.go b/pkg/machine/wsl/machine_unsupported.go new file mode 100644 index 000000000..043c5d729 --- /dev/null +++ b/pkg/machine/wsl/machine_unsupported.go @@ -0,0 +1,3 @@ +// +build !windows + +package wsl diff --git a/pkg/machine/wsl/util_windows.go b/pkg/machine/wsl/util_windows.go new file mode 100644 index 000000000..95e4c9894 --- /dev/null +++ b/pkg/machine/wsl/util_windows.go @@ -0,0 +1,338 @@ +package wsl + +import ( + "encoding/base64" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "syscall" + "unicode/utf16" + "unsafe" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" + + "github.com/containers/storage/pkg/homedir" +) + +//nolint +type SHELLEXECUTEINFO struct { + cbSize uint32 + fMask uint32 + hwnd syscall.Handle + lpVerb uintptr + lpFile uintptr + lpParameters uintptr + lpDirectory uintptr + nShow int + hInstApp syscall.Handle + lpIDList uintptr + lpClass uintptr + hkeyClass syscall.Handle + dwHotKey uint32 + hIconOrMonitor syscall.Handle + hProcess syscall.Handle +} + +//nolint +type Luid struct { + lowPart uint32 + highPart int32 +} + +type LuidAndAttributes struct { + luid Luid + attributes uint32 +} + +type TokenPrivileges struct { + privilegeCount uint32 + privileges [1]LuidAndAttributes +} + +//nolint // Cleaner to refer to the official OS constant names, and consistent with syscall +const ( + SEE_MASK_NOCLOSEPROCESS = 0x40 + EWX_FORCEIFHUNG = 0x10 + EWX_REBOOT = 0x02 + EWX_RESTARTAPPS = 0x40 + SHTDN_REASON_MAJOR_APPLICATION = 0x00040000 + SHTDN_REASON_MINOR_INSTALLATION = 0x00000002 + SHTDN_REASON_FLAG_PLANNED = 0x80000000 + TOKEN_ADJUST_PRIVILEGES = 0x0020 + TOKEN_QUERY = 0x0008 + SE_PRIVILEGE_ENABLED = 0x00000002 + SE_ERR_ACCESSDENIED = 0x05 +) + +func winVersionAtLeast(major uint, minor uint, build uint) bool { + var out [3]uint32 + + in := []uint32{uint32(major), uint32(minor), uint32(build)} + out[0], out[1], out[2] = windows.RtlGetNtVersionNumbers() + + for i, o := range out { + if in[i] > o { + return false + } + if in[i] < o { + return true + } + } + + return true +} + +func hasAdminRights() bool { + var sid *windows.SID + + // See: https://coolaj86.com/articles/golang-and-windows-and-admins-oh-my/ + if err := windows.AllocateAndInitializeSid( + &windows.SECURITY_NT_AUTHORITY, + 2, + windows.SECURITY_BUILTIN_DOMAIN_RID, + windows.DOMAIN_ALIAS_RID_ADMINS, + 0, 0, 0, 0, 0, 0, + &sid); err != nil { + logrus.Warnf("SID allocation error: %s", err) + return false + } + defer windows.FreeSid(sid) + + // From MS docs: + // "If TokenHandle is NULL, CheckTokenMembership uses the impersonation + // token of the calling thread. If the thread is not impersonating, + // the function duplicates the thread's primary token to create an + // impersonation token." + token := windows.Token(0) + + member, err := token.IsMember(sid) + if err != nil { + logrus.Warnf("Token Membership Error: %s", err) + return false + } + + return member || token.IsElevated() +} + +func relaunchElevatedWait() error { + e, _ := os.Executable() + d, _ := os.Getwd() + exe, _ := syscall.UTF16PtrFromString(e) + cwd, _ := syscall.UTF16PtrFromString(d) + arg, _ := syscall.UTF16PtrFromString(buildCommandArgs(true)) + verb, _ := syscall.UTF16PtrFromString("runas") + + shell32 := syscall.NewLazyDLL("shell32.dll") + + info := &SHELLEXECUTEINFO{ + fMask: SEE_MASK_NOCLOSEPROCESS, + hwnd: 0, + lpVerb: uintptr(unsafe.Pointer(verb)), + lpFile: uintptr(unsafe.Pointer(exe)), + lpParameters: uintptr(unsafe.Pointer(arg)), + lpDirectory: uintptr(unsafe.Pointer(cwd)), + nShow: 1, + } + info.cbSize = uint32(unsafe.Sizeof(*info)) + procShellExecuteEx := shell32.NewProc("ShellExecuteExW") + if ret, _, _ := procShellExecuteEx.Call(uintptr(unsafe.Pointer(info))); ret == 0 { // 0 = False + err := syscall.GetLastError() + if info.hInstApp == SE_ERR_ACCESSDENIED { + return wrapMaybe(err, "request to elevate privileges was denied") + } + return wrapMaybef(err, "could not launch process, ShellEX Error = %d", info.hInstApp) + } + + handle := syscall.Handle(info.hProcess) + defer syscall.CloseHandle(handle) + + w, err := syscall.WaitForSingleObject(handle, syscall.INFINITE) + switch w { + case syscall.WAIT_OBJECT_0: + break + case syscall.WAIT_FAILED: + return errors.Wrap(err, "could not wait for process, failed") + default: + return errors.Errorf("could not wait for process, unknown error") + } + var code uint32 + if err := syscall.GetExitCodeProcess(handle, &code); err != nil { + return err + } + if code != 0 { + return &ExitCodeError{uint(code)} + } + + return nil +} + +func wrapMaybe(err error, message string) error { + if err != nil { + return errors.Wrap(err, message) + } + + return errors.New(message) +} + +func wrapMaybef(err error, format string, args ...interface{}) error { + if err != nil { + return errors.Wrapf(err, format, args...) + } + + return errors.Errorf(format, args...) +} + +func reboot() error { + const ( + wtLocation = `Microsoft\WindowsApps\wt.exe` + wtPrefix = `%LocalAppData%\Microsoft\WindowsApps\wt -p "Windows PowerShell" ` + localAppData = "LocalAppData" + pShellLaunch = `powershell -noexit "powershell -EncodedCommand (Get-Content '%s')"` + ) + + exe, _ := os.Executable() + relaunch := fmt.Sprintf("& %s %s", syscall.EscapeArg(exe), buildCommandArgs(false)) + encoded := base64.StdEncoding.EncodeToString(encodeUTF16Bytes(relaunch)) + + dataDir, err := homedir.GetDataHome() + if err != nil { + return errors.Wrap(err, "could not determine data directory") + } + if err := os.MkdirAll(dataDir, 0755); err != nil { + return errors.Wrap(err, "could not create data directory") + } + commFile := filepath.Join(dataDir, "podman-relaunch.dat") + if err := ioutil.WriteFile(commFile, []byte(encoded), 0600); err != nil { + return errors.Wrap(err, "could not serialize command state") + } + + command := fmt.Sprintf(pShellLaunch, commFile) + if _, err := os.Lstat(filepath.Join(os.Getenv(localAppData), wtLocation)); err == nil { + wtCommand := wtPrefix + command + // RunOnce is limited to 260 chars (supposedly no longer in Builds >= 19489) + // For now fallbacak in cases of long usernames (>89 chars) + if len(wtCommand) < 260 { + command = wtCommand + } + } + + if err := addRunOnceRegistryEntry(command); err != nil { + return err + } + + if err := obtainShutdownPrivilege(); err != nil { + return err + } + + message := "To continue the process of enabling WSL, the system needs to reboot. " + + "Alternatively, you can cancel and reboot manually\n\n" + + "After rebooting, please wait a minute or two for podman machine to relaunch and continue installing." + + if MessageBox(message, "Podman Machine", false) != 1 { + fmt.Println("Reboot is required to continue installation, please reboot at your convenience") + os.Exit(ErrorSuccessRebootRequired) + return nil + } + + user32 := syscall.NewLazyDLL("user32") + procExit := user32.NewProc("ExitWindowsEx") + if ret, _, err := procExit.Call(EWX_REBOOT|EWX_RESTARTAPPS|EWX_FORCEIFHUNG, + SHTDN_REASON_MAJOR_APPLICATION|SHTDN_REASON_MINOR_INSTALLATION|SHTDN_REASON_FLAG_PLANNED); ret != 1 { + return errors.Wrap(err, "reboot failed") + } + + return nil +} + +func obtainShutdownPrivilege() error { + const SeShutdownName = "SeShutdownPrivilege" + + advapi32 := syscall.NewLazyDLL("advapi32") + OpenProcessToken := advapi32.NewProc("OpenProcessToken") + LookupPrivilegeValue := advapi32.NewProc("LookupPrivilegeValueW") + AdjustTokenPrivileges := advapi32.NewProc("AdjustTokenPrivileges") + + proc, _ := syscall.GetCurrentProcess() + + var hToken uintptr + if ret, _, err := OpenProcessToken.Call(uintptr(proc), TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY, uintptr(unsafe.Pointer(&hToken))); ret != 1 { + return errors.Wrap(err, "error opening process token") + } + + var privs TokenPrivileges + if ret, _, err := LookupPrivilegeValue.Call(uintptr(0), uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(SeShutdownName))), uintptr(unsafe.Pointer(&(privs.privileges[0].luid)))); ret != 1 { + return errors.Wrap(err, "error looking up shutdown privilege") + } + + privs.privilegeCount = 1 + privs.privileges[0].attributes = SE_PRIVILEGE_ENABLED + + if ret, _, err := AdjustTokenPrivileges.Call(hToken, 0, uintptr(unsafe.Pointer(&privs)), 0, uintptr(0), 0); ret != 1 { + return errors.Wrap(err, "error enabling shutdown privilege on token") + } + + return nil +} + +func addRunOnceRegistryEntry(command string) error { + k, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\RunOnce`, registry.WRITE) + if err != nil { + return errors.Wrap(err, "could not open RunOnce registry entry") + } + + defer k.Close() + + if err := k.SetExpandStringValue("podman-machine", command); err != nil { + return errors.Wrap(err, "could not open RunOnce registry entry") + } + + return nil +} + +func encodeUTF16Bytes(s string) []byte { + u16 := utf16.Encode([]rune(s)) + u16le := make([]byte, len(u16)*2) + for i := 0; i < len(u16); i++ { + u16le[i<<1] = byte(u16[i]) + u16le[(i<<1)+1] = byte(u16[i] >> 8) + } + return u16le +} + +func MessageBox(caption, title string, fail bool) int { + var format int + if fail { + format = 0x10 + } else { + format = 0x41 + } + + user32 := syscall.NewLazyDLL("user32.dll") + captionPtr, _ := syscall.UTF16PtrFromString(caption) + titlePtr, _ := syscall.UTF16PtrFromString(title) + ret, _, _ := user32.NewProc("MessageBoxW").Call( + uintptr(0), + uintptr(unsafe.Pointer(captionPtr)), + uintptr(unsafe.Pointer(titlePtr)), + uintptr(format)) + + return int(ret) +} + +func buildCommandArgs(elevate bool) string { + var args []string + for _, arg := range os.Args[1:] { + if arg != "--reexec" { + args = append(args, syscall.EscapeArg(arg)) + if elevate && arg == "init" { + args = append(args, "--reexec") + } + } + } + return strings.Join(args, " ") +} diff --git a/pkg/network/network.go b/pkg/network/network.go deleted file mode 100644 index 44132ca28..000000000 --- a/pkg/network/network.go +++ /dev/null @@ -1,27 +0,0 @@ -package network - -import ( - "crypto/sha256" - "encoding/hex" - "strings" - - "github.com/containernetworking/cni/libcni" -) - -// GetCNIPlugins returns a list of plugins that a given network -// has in the form of a string -func GetCNIPlugins(list *libcni.NetworkConfigList) string { - plugins := make([]string, 0, len(list.Plugins)) - for _, plug := range list.Plugins { - plugins = append(plugins, plug.Network.Type) - } - return strings.Join(plugins, ",") -} - -// GetNetworkID return the network ID for a given name. -// It is just the sha256 hash but this should be good enough. -// The caller has to make sure it is only called with the network name. -func GetNetworkID(name string) string { - hash := sha256.Sum256([]byte(name)) - return hex.EncodeToString(hash[:]) -} diff --git a/pkg/ps/ps.go b/pkg/ps/ps.go index 90ad23f49..a1d77e785 100644 --- a/pkg/ps/ps.go +++ b/pkg/ps/ps.go @@ -74,7 +74,7 @@ func GetContainerLists(runtime *libpod.Runtime, options entities.ContainerListOp } } - if options.All && options.External { + if options.External { listCon, err := GetExternalContainerLists(runtime) if err != nil { return nil, err @@ -207,7 +207,7 @@ func ListContainerBatch(rt *libpod.Runtime, ctr *libpod.Container, opts entities return entities.ListContainer{}, err } - networks, _, err := ctr.Networks() + networks, err := ctr.Networks() if err != nil { return entities.ListContainer{}, err } diff --git a/pkg/rootless/rootless_linux.c b/pkg/rootless/rootless_linux.c index 92f331ce4..94bd40f86 100644 --- a/pkg/rootless/rootless_linux.c +++ b/pkg/rootless/rootless_linux.c @@ -244,7 +244,7 @@ can_use_shortcut () if (argv[argc+1] != NULL && (strcmp (argv[argc], "container") == 0 || strcmp (argv[argc], "image") == 0) && - strcmp (argv[argc+1], "mount") == 0) + (strcmp (argv[argc+1], "mount") == 0 || strcmp (argv[argc+1], "scp") == 0)) { ret = false; break; diff --git a/pkg/rootless/rootless_linux.go b/pkg/rootless/rootless_linux.go index 3e81d5c14..92725adc7 100644 --- a/pkg/rootless/rootless_linux.go +++ b/pkg/rootless/rootless_linux.go @@ -145,8 +145,8 @@ func tryMappingTool(uid bool, pid int, hostID int, mappings []idtools.IDMap) err } if output, err := cmd.CombinedOutput(); err != nil { - logrus.Debugf("error from %s: %s", tool, output) - return errors.Wrapf(err, "cannot setup namespace using %s", tool) + logrus.Errorf("error running `%s`: %s", strings.Join(args, " "), output) + return errors.Wrapf(err, "cannot setup namespace using %q", path) } return nil } diff --git a/pkg/specgen/container_validate.go b/pkg/specgen/container_validate.go index caea51ea8..d06a047c1 100644 --- a/pkg/specgen/container_validate.go +++ b/pkg/specgen/container_validate.go @@ -29,25 +29,10 @@ func exclusiveOptions(opt1, opt2 string) error { // Validate verifies that the given SpecGenerator is valid and satisfies required // input for creating a container. func (s *SpecGenerator) Validate() error { - if rootless.IsRootless() && len(s.CNINetworks) == 0 { - if s.StaticIP != nil || s.StaticIPv6 != nil { - return ErrNoStaticIPRootless - } - if s.StaticMAC != nil { - return ErrNoStaticMACRootless - } - } - // Containers being added to a pod cannot have certain network attributes // associated with them because those should be on the infra container. if len(s.Pod) > 0 && s.NetNS.NSMode == FromPod { - if s.StaticIP != nil || s.StaticIPv6 != nil { - return errors.Wrap(define.ErrNetworkOnPodContainer, "static ip addresses must be defined when the pod is created") - } - if s.StaticMAC != nil { - return errors.Wrap(define.ErrNetworkOnPodContainer, "MAC addresses must be defined when the pod is created") - } - if len(s.CNINetworks) > 0 { + if len(s.Networks) > 0 { return errors.Wrap(define.ErrNetworkOnPodContainer, "networks must be defined when the pod is created") } if len(s.PortMappings) > 0 || s.PublishExposedPorts { @@ -204,5 +189,10 @@ func (s *SpecGenerator) Validate() error { if err := validateNetNS(&s.NetNS); err != nil { return err } + if s.NetNS.NSMode != Bridge && len(s.Networks) > 0 { + // Note that we also get the ip and mac in the networks map + return errors.New("Networks and static ip/mac address can only be used with Bridge mode networking") + } + return nil } diff --git a/pkg/specgen/generate/container.go b/pkg/specgen/generate/container.go index 40a18a6ac..57676db10 100644 --- a/pkg/specgen/generate/container.go +++ b/pkg/specgen/generate/container.go @@ -156,7 +156,9 @@ func CompleteSpec(ctx context.Context, r *libpod.Runtime, s *specgen.SpecGenerat // Add annotations from the image for k, v := range inspectData.Annotations { - annotations[k] = v + if !define.IsReservedAnnotation(k) { + annotations[k] = v + } } } diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index df5d2e8ff..7d792b3b1 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -2,13 +2,15 @@ package generate import ( "context" - "fmt" + "encoding/json" "path/filepath" "strings" cdi "github.com/container-orchestrated-devices/container-device-interface/pkg" "github.com/containers/common/libimage" "github.com/containers/podman/v3/libpod" + "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/pkg/namespaces" "github.com/containers/podman/v3/pkg/specgen" "github.com/containers/podman/v3/pkg/util" spec "github.com/opencontainers/runtime-spec/specs-go" @@ -28,43 +30,30 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener // If joining a pod, retrieve the pod for use, and its infra container var pod *libpod.Pod - var infraConfig *libpod.ContainerConfig + var infra *libpod.Container if s.Pod != "" { pod, err = rt.LookupPod(s.Pod) if err != nil { return nil, nil, nil, errors.Wrapf(err, "error retrieving pod %s", s.Pod) } if pod.HasInfraContainer() { - infra, err := pod.InfraContainer() + infra, err = pod.InfraContainer() if err != nil { return nil, nil, nil, err } - infraConfig = infra.Config() } } - if infraConfig != nil && (len(infraConfig.NamedVolumes) > 0 || len(infraConfig.UserVolumes) > 0 || len(infraConfig.ImageVolumes) > 0 || len(infraConfig.OverlayVolumes) > 0) { - s.VolumesFrom = append(s.VolumesFrom, infraConfig.ID) - } - - if infraConfig != nil && len(infraConfig.Spec.Linux.Devices) > 0 { - s.DevicesFrom = append(s.DevicesFrom, infraConfig.ID) - } - if infraConfig != nil && infraConfig.Spec.Linux.Resources != nil && infraConfig.Spec.Linux.Resources.BlockIO != nil && len(infraConfig.Spec.Linux.Resources.BlockIO.ThrottleReadBpsDevice) > 0 { - tempDev := make(map[string]spec.LinuxThrottleDevice) - for _, val := range infraConfig.Spec.Linux.Resources.BlockIO.ThrottleReadBpsDevice { - nodes, err := util.FindDeviceNodes() - if err != nil { - return nil, nil, nil, err - } - key := fmt.Sprintf("%d:%d", val.Major, val.Minor) - tempDev[nodes[key]] = spec.LinuxThrottleDevice{Rate: uint64(val.Rate)} - } - for i, dev := range s.ThrottleReadBpsDevice { - tempDev[i] = dev + options := []libpod.CtrCreateOption{} + compatibleOptions := &libpod.InfraInherit{} + var infraSpec *spec.Spec + if infra != nil { + options, infraSpec, compatibleOptions, err = Inherit(*infra) + if err != nil { + return nil, nil, nil, err } - s.ThrottleReadBpsDevice = tempDev } + if err := FinishThrottleDevices(s); err != nil { return nil, nil, nil, err } @@ -96,6 +85,12 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener return nil, nil, nil, err } s.UserNS = defaultNS + + mappings, err := util.ParseIDMapping(namespaces.UsernsMode(s.UserNS.NSMode), nil, nil, "", "") + if err != nil { + return nil, nil, nil, err + } + s.IDMappings = mappings } if s.NetNS.IsDefault() { defaultNS, err := GetDefaultNamespaceMode("net", rtc, pod) @@ -112,8 +107,6 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener s.CgroupNS = defaultNS } - options := []libpod.CtrCreateOption{} - if s.ContainerCreateCommand != nil { options = append(options, libpod.WithCreateCommand(s.ContainerCreateCommand)) } @@ -149,21 +142,22 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener return nil, nil, nil, err } + if len(s.HostUsers) > 0 { + options = append(options, libpod.WithHostUsers(s.HostUsers)) + } + command, err := makeCommand(ctx, s, imageData, rtc) if err != nil { return nil, nil, nil, err } - opts, err := createContainerOptions(ctx, rt, s, pod, finalVolumes, finalOverlays, imageData, command) + infraVolumes := (len(compatibleOptions.InfraVolumes) > 0 || len(compatibleOptions.InfraUserVolumes) > 0 || len(compatibleOptions.InfraImageVolumes) > 0) + opts, err := createContainerOptions(ctx, rt, s, pod, finalVolumes, finalOverlays, imageData, command, infraVolumes, *compatibleOptions) if err != nil { return nil, nil, nil, err } options = append(options, opts...) - if len(s.Aliases) > 0 { - options = append(options, libpod.WithNetworkAliases(s.Aliases)) - } - if containerType := s.InitContainerType; len(containerType) > 0 { options = append(options, libpod.WithInitCtrType(containerType)) } @@ -171,27 +165,29 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener logrus.Debugf("setting container name %s", s.Name) options = append(options, libpod.WithName(s.Name)) } - if len(s.DevicesFrom) > 0 { - for _, dev := range s.DevicesFrom { - ctr, err := rt.GetContainer(dev) - if err != nil { - return nil, nil, nil, err - } - devices := ctr.DeviceHostSrc() - s.Devices = append(s.Devices, devices...) - } - } if len(s.Devices) > 0 { - opts = extractCDIDevices(s) + opts = ExtractCDIDevices(s) options = append(options, opts...) } - runtimeSpec, err := SpecGenToOCI(ctx, s, rt, rtc, newImage, finalMounts, pod, command) + runtimeSpec, err := SpecGenToOCI(ctx, s, rt, rtc, newImage, finalMounts, pod, command, compatibleOptions) if err != nil { return nil, nil, nil, err } if len(s.HostDeviceList) > 0 { options = append(options, libpod.WithHostDevice(s.HostDeviceList)) } + if infraSpec != nil && infraSpec.Linux != nil { // if we are inheriting Linux info from a pod... + // Pass Security annotations + if len(infraSpec.Annotations[define.InspectAnnotationLabel]) > 0 && len(runtimeSpec.Annotations[define.InspectAnnotationLabel]) == 0 { + runtimeSpec.Annotations[define.InspectAnnotationLabel] = infraSpec.Annotations[define.InspectAnnotationLabel] + } + if len(infraSpec.Annotations[define.InspectAnnotationSeccomp]) > 0 && len(runtimeSpec.Annotations[define.InspectAnnotationSeccomp]) == 0 { + runtimeSpec.Annotations[define.InspectAnnotationSeccomp] = infraSpec.Annotations[define.InspectAnnotationSeccomp] + } + if len(infraSpec.Annotations[define.InspectAnnotationApparmor]) > 0 && len(runtimeSpec.Annotations[define.InspectAnnotationApparmor]) == 0 { + runtimeSpec.Annotations[define.InspectAnnotationApparmor] = infraSpec.Annotations[define.InspectAnnotationApparmor] + } + } return runtimeSpec, s, options, err } func ExecuteCreate(ctx context.Context, rt *libpod.Runtime, runtimeSpec *spec.Spec, s *specgen.SpecGenerator, infra bool, options ...libpod.CtrCreateOption) (*libpod.Container, error) { @@ -203,7 +199,7 @@ func ExecuteCreate(ctx context.Context, rt *libpod.Runtime, runtimeSpec *spec.Sp return ctr, rt.PrepareVolumeOnCreateContainer(ctx, ctr) } -func extractCDIDevices(s *specgen.SpecGenerator) []libpod.CtrCreateOption { +func ExtractCDIDevices(s *specgen.SpecGenerator) []libpod.CtrCreateOption { devs := make([]spec.LinuxDevice, 0, len(s.Devices)) var cdiDevs []string var options []libpod.CtrCreateOption @@ -217,19 +213,16 @@ func extractCDIDevices(s *specgen.SpecGenerator) []libpod.CtrCreateOption { cdiDevs = append(cdiDevs, device.Path) continue } - devs = append(devs, device) } - s.Devices = devs if len(cdiDevs) > 0 { options = append(options, libpod.WithCDI(cdiDevs)) } - return options } -func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGenerator, pod *libpod.Pod, volumes []*specgen.NamedVolume, overlays []*specgen.OverlayVolume, imageData *libimage.ImageData, command []string) ([]libpod.CtrCreateOption, error) { +func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGenerator, pod *libpod.Pod, volumes []*specgen.NamedVolume, overlays []*specgen.OverlayVolume, imageData *libimage.ImageData, command []string, infraVolumes bool, compatibleOptions libpod.InfraInherit) ([]libpod.CtrCreateOption, error) { var options []libpod.CtrCreateOption var err error @@ -310,7 +303,10 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen. for _, imageVolume := range s.ImageVolumes { destinations = append(destinations, imageVolume.Destination) } - options = append(options, libpod.WithUserVolumes(destinations)) + + if len(destinations) > 0 || !infraVolumes { + options = append(options, libpod.WithUserVolumes(destinations)) + } if len(volumes) != 0 { var vols []*libpod.ContainerNamedVolume @@ -398,7 +394,7 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen. if len(s.SelinuxOpts) > 0 { options = append(options, libpod.WithSecLabels(s.SelinuxOpts)) } else { - if pod != nil { + if pod != nil && len(compatibleOptions.InfraLabels) == 0 { // duplicate the security options from the pod processLabel, err := pod.ProcessLabel() if err != nil { @@ -486,5 +482,38 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen. if s.PidFile != "" { options = append(options, libpod.WithPidFile(s.PidFile)) } + + options = append(options, libpod.WithSelectedPasswordManagement(s.Passwd)) + return options, nil } + +func Inherit(infra libpod.Container) (opts []libpod.CtrCreateOption, infraS *spec.Spec, compat *libpod.InfraInherit, err error) { + options := []libpod.CtrCreateOption{} + compatibleOptions := &libpod.InfraInherit{} + infraConf := infra.Config() + infraSpec := infraConf.Spec + + config, err := json.Marshal(infraConf) + if err != nil { + return nil, nil, nil, err + } + err = json.Unmarshal(config, compatibleOptions) + if err != nil { + return nil, nil, nil, err + } + if infraSpec.Linux != nil && infraSpec.Linux.Resources != nil { + resources, err := json.Marshal(infraSpec.Linux.Resources) + if err != nil { + return nil, nil, nil, err + } + err = json.Unmarshal(resources, &compatibleOptions.InfraResources) + if err != nil { + return nil, nil, nil, err + } + } + if compatibleOptions != nil { + options = append(options, libpod.WithInfraConfig(*compatibleOptions)) + } + return options, infraSpec, compatibleOptions, nil +} diff --git a/pkg/specgen/generate/namespaces.go b/pkg/specgen/generate/namespaces.go index 7d63fc10f..a2bc37e34 100644 --- a/pkg/specgen/generate/namespaces.go +++ b/pkg/specgen/generate/namespaces.go @@ -10,6 +10,7 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/podman/v3/libpod" "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/pkg/rootless" "github.com/containers/podman/v3/pkg/specgen" "github.com/containers/podman/v3/pkg/util" @@ -66,7 +67,7 @@ func GetDefaultNamespaceMode(nsType string, cfg *config.Config, pod *libpod.Pod) case "cgroup": return specgen.ParseCgroupNamespace(cfg.Containers.CgroupNS) case "net": - ns, _, err := specgen.ParseNetworkNamespace(cfg.Containers.NetNS, cfg.Containers.RootlessNetworking == "cni") + ns, _, _, err := specgen.ParseNetworkFlag(nil) return ns, err } @@ -250,7 +251,7 @@ func namespaceOptions(ctx context.Context, s *specgen.SpecGenerator, rt *libpod. if s.NetNS.Value != "" { val = fmt.Sprintf("slirp4netns:%s", s.NetNS.Value) } - toReturn = append(toReturn, libpod.WithNetNS(portMappings, expose, postConfigureNetNS, val, s.CNINetworks)) + toReturn = append(toReturn, libpod.WithNetNS(portMappings, expose, postConfigureNetNS, val, nil)) case specgen.Private: fallthrough case specgen.Bridge: @@ -258,7 +259,34 @@ func namespaceOptions(ctx context.Context, s *specgen.SpecGenerator, rt *libpod. if err != nil { return nil, err } - toReturn = append(toReturn, libpod.WithNetNS(portMappings, expose, postConfigureNetNS, "bridge", s.CNINetworks)) + + rtConfig, err := rt.GetConfigNoCopy() + if err != nil { + return nil, err + } + // if no network was specified use add the default + if len(s.Networks) == 0 { + // backwards config still allow the old cni networks list and convert to new format + if len(s.CNINetworks) > 0 { + logrus.Warn(`specgen "cni_networks" option is deprecated use the "networks" map instead`) + networks := make(map[string]types.PerNetworkOptions, len(s.CNINetworks)) + for _, net := range s.CNINetworks { + networks[net] = types.PerNetworkOptions{} + } + s.Networks = networks + } else { + // no networks given but bridge is set so use default network + s.Networks = map[string]types.PerNetworkOptions{ + rtConfig.Network.DefaultNetwork: {}, + } + } + } + // rename the "default" network to the correct default name + if opts, ok := s.Networks["default"]; ok { + s.Networks[rtConfig.Network.DefaultNetwork] = opts + delete(s.Networks, "default") + } + toReturn = append(toReturn, libpod.WithNetNS(portMappings, expose, postConfigureNetNS, "bridge", s.Networks)) } if s.UseImageHosts { @@ -281,12 +309,6 @@ func namespaceOptions(ctx context.Context, s *specgen.SpecGenerator, rt *libpod. if len(s.DNSOptions) > 0 { toReturn = append(toReturn, libpod.WithDNSOption(s.DNSOptions)) } - if s.StaticIP != nil { - toReturn = append(toReturn, libpod.WithStaticIP(*s.StaticIP)) - } - if s.StaticMAC != nil { - toReturn = append(toReturn, libpod.WithStaticMAC(*s.StaticMAC)) - } if s.NetworkOptions != nil { toReturn = append(toReturn, libpod.WithNetworkOptions(s.NetworkOptions)) } diff --git a/pkg/specgen/generate/oci.go b/pkg/specgen/generate/oci.go index 9f8807915..ee3a990fc 100644 --- a/pkg/specgen/generate/oci.go +++ b/pkg/specgen/generate/oci.go @@ -2,6 +2,7 @@ package generate import ( "context" + "encoding/json" "path" "strings" @@ -174,7 +175,7 @@ func getCGroupPermissons(unmask []string) string { } // SpecGenToOCI returns the base configuration for the container. -func SpecGenToOCI(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runtime, rtc *config.Config, newImage *libimage.Image, mounts []spec.Mount, pod *libpod.Pod, finalCmd []string) (*spec.Spec, error) { +func SpecGenToOCI(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runtime, rtc *config.Config, newImage *libimage.Image, mounts []spec.Mount, pod *libpod.Pod, finalCmd []string, compatibleOptions *libpod.InfraInherit) (*spec.Spec, error) { cgroupPerm := getCGroupPermissons(s.Unmask) g, err := generate.New("linux") @@ -299,9 +300,32 @@ func SpecGenToOCI(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runt g.AddAnnotation(key, val) } - g.Config.Linux.Resources = s.ResourceLimits + if compatibleOptions.InfraResources == nil && s.ResourceLimits != nil { + g.Config.Linux.Resources = s.ResourceLimits + } else if s.ResourceLimits != nil { // if we have predefined resource limits we need to make sure we keep the infra and container limits + originalResources, err := json.Marshal(s.ResourceLimits) + if err != nil { + return nil, err + } + infraResources, err := json.Marshal(compatibleOptions.InfraResources) + if err != nil { + return nil, err + } + err = json.Unmarshal(infraResources, s.ResourceLimits) // put infra's resource limits in the container + if err != nil { + return nil, err + } + err = json.Unmarshal(originalResources, s.ResourceLimits) // make sure we did not override anything + if err != nil { + return nil, err + } + g.Config.Linux.Resources = s.ResourceLimits + } else { + g.Config.Linux.Resources = compatibleOptions.InfraResources + } // Devices + var userDevices []spec.LinuxDevice if s.Privileged { // If privileged, we need to add all the host devices to the // spec. We do not add the user provided ones because we are @@ -316,17 +340,26 @@ func SpecGenToOCI(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runt return nil, err } } + if len(compatibleOptions.InfraDevices) > 0 && len(s.Devices) == 0 { + userDevices = compatibleOptions.InfraDevices + } else { + userDevices = s.Devices + } // add default devices specified by caller - for _, device := range s.Devices { + for _, device := range userDevices { if err = DevicesFromPath(&g, device.Path); err != nil { return nil, err } } } - s.HostDeviceList = s.Devices + s.HostDeviceList = userDevices - for _, dev := range s.DeviceCGroupRule { - g.AddLinuxResourcesDevice(true, dev.Type, dev.Major, dev.Minor, dev.Access) + // set the devices cgroup when not running in a user namespace + if !inUserNS && !s.Privileged { + g.AddLinuxResourcesDevice(false, "", nil, nil, "rwm") + for _, dev := range s.DeviceCGroupRule { + g.AddLinuxResourcesDevice(true, dev.Type, dev.Major, dev.Minor, dev.Access) + } } for k, v := range s.WeightDevice { diff --git a/pkg/specgen/generate/pod_create.go b/pkg/specgen/generate/pod_create.go index 72dd249e7..0a797c571 100644 --- a/pkg/specgen/generate/pod_create.go +++ b/pkg/specgen/generate/pod_create.go @@ -218,9 +218,7 @@ func MapSpec(p *specgen.PodSpecGenerator) (*specgen.SpecGenerator, error) { case specgen.Host: logrus.Debugf("Pod will use host networking") if len(p.InfraContainerSpec.PortMappings) > 0 || - p.InfraContainerSpec.StaticIP != nil || - p.InfraContainerSpec.StaticMAC != nil || - len(p.InfraContainerSpec.CNINetworks) > 0 || + len(p.InfraContainerSpec.Networks) > 0 || p.InfraContainerSpec.NetNS.NSMode == specgen.NoNetwork { return nil, errors.Wrapf(define.ErrInvalidArg, "cannot set host network if network-related configuration is specified") } @@ -234,9 +232,7 @@ func MapSpec(p *specgen.PodSpecGenerator) (*specgen.SpecGenerator, error) { case specgen.NoNetwork: logrus.Debugf("Pod will not use networking") if len(p.InfraContainerSpec.PortMappings) > 0 || - p.InfraContainerSpec.StaticIP != nil || - p.InfraContainerSpec.StaticMAC != nil || - len(p.InfraContainerSpec.CNINetworks) > 0 || + len(p.InfraContainerSpec.Networks) > 0 || p.InfraContainerSpec.NetNS.NSMode == "host" { return nil, errors.Wrapf(define.ErrInvalidArg, "cannot disable pod network if network-related configuration is specified") } @@ -264,15 +260,13 @@ func MapSpec(p *specgen.PodSpecGenerator) (*specgen.SpecGenerator, error) { if len(p.DNSSearch) > 0 { p.InfraContainerSpec.DNSSearch = p.DNSSearch } - if p.StaticIP != nil { - p.InfraContainerSpec.StaticIP = p.StaticIP - } - if p.StaticMAC != nil { - p.InfraContainerSpec.StaticMAC = p.StaticMAC - } if p.NoManageResolvConf { p.InfraContainerSpec.UseImageResolvConf = true } + if len(p.Networks) > 0 { + p.InfraContainerSpec.Networks = p.Networks + } + // deprecated cni networks for api users if len(p.CNINetworks) > 0 { p.InfraContainerSpec.CNINetworks = p.CNINetworks } diff --git a/pkg/specgen/generate/validate.go b/pkg/specgen/generate/validate.go index a44bf9979..c74db7325 100644 --- a/pkg/specgen/generate/validate.go +++ b/pkg/specgen/generate/validate.go @@ -60,10 +60,6 @@ func verifyContainerResourcesCgroupV1(s *specgen.SpecGenerator) ([]string, error if memory.Limit != nil && memory.Reservation != nil && *memory.Limit < *memory.Reservation { return warnings, errors.New("minimum memory limit cannot be less than memory reservation limit, see usage") } - if memory.Kernel != nil && !sysInfo.KernelMemory { - warnings = append(warnings, "Your kernel does not support kernel memory limit capabilities or the cgroup is not mounted. Limitation discarded.") - memory.Kernel = nil - } if memory.DisableOOMKiller != nil && *memory.DisableOOMKiller && !sysInfo.OomKillDisable { warnings = append(warnings, "Your kernel does not support OomKillDisable. OomKillDisable discarded.") memory.DisableOOMKiller = nil diff --git a/pkg/specgen/namespaces.go b/pkg/specgen/namespaces.go index bb5385ef1..15a8ece17 100644 --- a/pkg/specgen/namespaces.go +++ b/pkg/specgen/namespaces.go @@ -2,10 +2,13 @@ package specgen import ( "fmt" + "net" "os" "strings" "github.com/containers/common/pkg/cgroups" + "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/pkg/rootless" "github.com/containers/podman/v3/pkg/util" "github.com/containers/storage" @@ -271,9 +274,9 @@ func ParseUserNamespace(ns string) (Namespace, error) { // ParseNetworkNamespace parses a network namespace specification in string // form. // Returns a namespace and (optionally) a list of CNI networks to join. -func ParseNetworkNamespace(ns string, rootlessDefaultCNI bool) (Namespace, []string, error) { +func ParseNetworkNamespace(ns string, rootlessDefaultCNI bool) (Namespace, map[string]types.PerNetworkOptions, error) { toReturn := Namespace{} - var cniNetworks []string + networks := make(map[string]types.PerNetworkOptions) // Net defaults to Slirp on rootless switch { case ns == string(Slirp), strings.HasPrefix(ns, string(Slirp)+":"): @@ -313,28 +316,174 @@ func ParseNetworkNamespace(ns string, rootlessDefaultCNI bool) (Namespace, []str default: // Assume we have been given a list of CNI networks. // Which only works in bridge mode, so set that. - cniNetworks = strings.Split(ns, ",") + networkList := strings.Split(ns, ",") + for _, net := range networkList { + networks[net] = types.PerNetworkOptions{} + } + toReturn.NSMode = Bridge } - return toReturn, cniNetworks, nil + return toReturn, networks, nil } -func ParseNetworkString(network string) (Namespace, []string, map[string][]string, error) { +// ParseNetworkFlag parses a network string slice into the network options +// If the input is nil or empty it will use the default setting from containers.conf +func ParseNetworkFlag(networks []string) (Namespace, map[string]types.PerNetworkOptions, map[string][]string, error) { var networkOptions map[string][]string - parts := strings.SplitN(network, ":", 2) + // by default we try to use the containers.conf setting + // if we get at least one value use this instead + ns := containerConfig.Containers.NetNS + if len(networks) > 0 { + ns = networks[0] + } - ns, cniNets, err := ParseNetworkNamespace(network, containerConfig.Containers.RootlessNetworking == "cni") - if err != nil { - return Namespace{}, nil, nil, err + toReturn := Namespace{} + podmanNetworks := make(map[string]types.PerNetworkOptions) + + switch { + case ns == string(Slirp), strings.HasPrefix(ns, string(Slirp)+":"): + parts := strings.SplitN(ns, ":", 2) + if len(parts) > 1 { + networkOptions = make(map[string][]string) + networkOptions[parts[0]] = strings.Split(parts[1], ",") + } + toReturn.NSMode = Slirp + case ns == string(FromPod): + toReturn.NSMode = FromPod + case ns == "" || ns == string(Default) || ns == string(Private): + // Net defaults to Slirp on rootless + if rootless.IsRootless() && containerConfig.Containers.RootlessNetworking != "cni" { + toReturn.NSMode = Slirp + break + } + // if not slirp we use bridge + fallthrough + case ns == string(Bridge), strings.HasPrefix(ns, string(Bridge)+":"): + toReturn.NSMode = Bridge + parts := strings.SplitN(ns, ":", 2) + netOpts := types.PerNetworkOptions{} + if len(parts) > 1 { + var err error + netOpts, err = parseBridgeNetworkOptions(parts[1]) + if err != nil { + return toReturn, nil, nil, err + } + } + // we have to set the special default network name here + podmanNetworks["default"] = netOpts + + case ns == string(NoNetwork): + toReturn.NSMode = NoNetwork + case ns == string(Host): + toReturn.NSMode = Host + case strings.HasPrefix(ns, "ns:"): + split := strings.SplitN(ns, ":", 2) + if len(split) != 2 { + return toReturn, nil, nil, errors.Errorf("must provide a path to a namespace when specifying ns:") + } + toReturn.NSMode = Path + toReturn.Value = split[1] + case strings.HasPrefix(ns, string(FromContainer)+":"): + split := strings.SplitN(ns, ":", 2) + if len(split) != 2 { + return toReturn, nil, nil, errors.Errorf("must provide name or ID or a container when specifying container:") + } + toReturn.NSMode = FromContainer + toReturn.Value = split[1] + default: + // we should have a normal network + parts := strings.SplitN(ns, ":", 2) + if len(parts) == 1 { + // Assume we have been given a comma separated list of networks for backwards compat. + networkList := strings.Split(ns, ",") + for _, net := range networkList { + podmanNetworks[net] = types.PerNetworkOptions{} + } + } else { + if parts[0] == "" { + return toReturn, nil, nil, errors.New("network name cannot be empty") + } + netOpts, err := parseBridgeNetworkOptions(parts[1]) + if err != nil { + return toReturn, nil, nil, errors.Wrapf(err, "invalid option for network %s", parts[0]) + } + podmanNetworks[parts[0]] = netOpts + } + + // networks need bridge mode + toReturn.NSMode = Bridge + } + + if len(networks) > 1 { + if !toReturn.IsBridge() { + return toReturn, nil, nil, errors.Wrapf(define.ErrInvalidArg, "cannot set multiple networks without bridge network mode, selected mode %s", toReturn.NSMode) + } + + for _, network := range networks[1:] { + parts := strings.SplitN(network, ":", 2) + if parts[0] == "" { + return toReturn, nil, nil, errors.Wrapf(define.ErrInvalidArg, "network name cannot be empty") + } + if util.StringInSlice(parts[0], []string{string(Bridge), string(Slirp), string(FromPod), string(NoNetwork), + string(Default), string(Private), string(Path), string(FromContainer), string(Host)}) { + return toReturn, nil, nil, errors.Wrapf(define.ErrInvalidArg, "can only set extra network names, selected mode %s conflicts with bridge", parts[0]) + } + netOpts := types.PerNetworkOptions{} + if len(parts) > 1 { + var err error + netOpts, err = parseBridgeNetworkOptions(parts[1]) + if err != nil { + return toReturn, nil, nil, errors.Wrapf(err, "invalid option for network %s", parts[0]) + } + } + podmanNetworks[parts[0]] = netOpts + } + } + + return toReturn, podmanNetworks, networkOptions, nil +} + +func parseBridgeNetworkOptions(opts string) (types.PerNetworkOptions, error) { + netOpts := types.PerNetworkOptions{} + if len(opts) == 0 { + return netOpts, nil } + allopts := strings.Split(opts, ",") + for _, opt := range allopts { + split := strings.SplitN(opt, "=", 2) + switch split[0] { + case "ip", "ip6": + ip := net.ParseIP(split[1]) + if ip == nil { + return netOpts, errors.Errorf("invalid ip address %q", split[1]) + } + netOpts.StaticIPs = append(netOpts.StaticIPs, ip) - if len(parts) > 1 { - networkOptions = make(map[string][]string) - networkOptions[parts[0]] = strings.Split(parts[1], ",") - cniNets = nil + case "mac": + mac, err := net.ParseMAC(split[1]) + if err != nil { + return netOpts, err + } + netOpts.StaticMAC = types.HardwareAddr(mac) + + case "alias": + if split[1] == "" { + return netOpts, errors.New("alias cannot be empty") + } + netOpts.Aliases = append(netOpts.Aliases, split[1]) + + case "interface_name": + if split[1] == "" { + return netOpts, errors.New("interface_name cannot be empty") + } + netOpts.InterfaceName = split[1] + + default: + return netOpts, errors.Errorf("unknown bridge network option: %s", split[0]) + } } - return ns, cniNets, networkOptions, nil + return netOpts, nil } func SetupUserNS(idmappings *storage.IDMappingOptions, userns Namespace, g *generate.Generator) (string, error) { diff --git a/pkg/specgen/namespaces_test.go b/pkg/specgen/namespaces_test.go new file mode 100644 index 000000000..4f69e6b98 --- /dev/null +++ b/pkg/specgen/namespaces_test.go @@ -0,0 +1,265 @@ +package specgen + +import ( + "net" + "testing" + + "github.com/containers/podman/v3/libpod/network/types" + "github.com/containers/podman/v3/pkg/rootless" + "github.com/stretchr/testify/assert" +) + +func parsMacNoErr(mac string) types.HardwareAddr { + m, _ := net.ParseMAC(mac) + return types.HardwareAddr(m) +} + +func TestParseNetworkFlag(t *testing.T) { + // root and rootless have different defaults + defaultNetName := "default" + defaultNetworks := map[string]types.PerNetworkOptions{ + defaultNetName: {}, + } + defaultNsMode := Namespace{NSMode: Bridge} + if rootless.IsRootless() { + defaultNsMode = Namespace{NSMode: Slirp} + defaultNetworks = map[string]types.PerNetworkOptions{} + } + + tests := []struct { + name string + args []string + nsmode Namespace + networks map[string]types.PerNetworkOptions + options map[string][]string + err string + }{ + { + name: "empty input", + args: nil, + nsmode: defaultNsMode, + networks: defaultNetworks, + }, + { + name: "empty string as input", + args: []string{}, + nsmode: defaultNsMode, + networks: defaultNetworks, + }, + { + name: "default mode", + args: []string{"default"}, + nsmode: defaultNsMode, + networks: defaultNetworks, + }, + { + name: "private mode", + args: []string{"private"}, + nsmode: defaultNsMode, + networks: defaultNetworks, + }, + { + name: "bridge mode", + args: []string{"bridge"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + defaultNetName: {}, + }, + }, + { + name: "slirp4netns mode", + args: []string{"slirp4netns"}, + nsmode: Namespace{NSMode: Slirp}, + networks: map[string]types.PerNetworkOptions{}, + }, + { + name: "from pod mode", + args: []string{"pod"}, + nsmode: Namespace{NSMode: FromPod}, + networks: map[string]types.PerNetworkOptions{}, + }, + { + name: "no network mode", + args: []string{"none"}, + nsmode: Namespace{NSMode: NoNetwork}, + networks: map[string]types.PerNetworkOptions{}, + }, + { + name: "container mode", + args: []string{"container:abc"}, + nsmode: Namespace{NSMode: FromContainer, Value: "abc"}, + networks: map[string]types.PerNetworkOptions{}, + }, + { + name: "ns path mode", + args: []string{"ns:/path"}, + nsmode: Namespace{NSMode: Path, Value: "/path"}, + networks: map[string]types.PerNetworkOptions{}, + }, + { + name: "slirp4netns mode with options", + args: []string{"slirp4netns:cidr=10.0.0.0/24"}, + nsmode: Namespace{NSMode: Slirp}, + networks: map[string]types.PerNetworkOptions{}, + options: map[string][]string{ + "slirp4netns": {"cidr=10.0.0.0/24"}, + }, + }, + { + name: "bridge mode with options 1", + args: []string{"bridge:ip=10.0.0.1,mac=11:22:33:44:55:66"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + defaultNetName: { + StaticIPs: []net.IP{net.ParseIP("10.0.0.1")}, + StaticMAC: parsMacNoErr("11:22:33:44:55:66"), + }, + }, + }, + { + name: "bridge mode with options 2", + args: []string{"bridge:ip=10.0.0.1,ip=10.0.0.5"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + defaultNetName: { + StaticIPs: []net.IP{net.ParseIP("10.0.0.1"), net.ParseIP("10.0.0.5")}, + }, + }, + }, + { + name: "bridge mode with ip6 option", + args: []string{"bridge:ip6=fd10::"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + defaultNetName: { + StaticIPs: []net.IP{net.ParseIP("fd10::")}, + }, + }, + }, + { + name: "bridge mode with alias option", + args: []string{"bridge:alias=myname,alias=myname2"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + defaultNetName: { + Aliases: []string{"myname", "myname2"}, + }, + }, + }, + { + name: "bridge mode with alias option", + args: []string{"bridge:alias=myname,alias=myname2"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + defaultNetName: { + Aliases: []string{"myname", "myname2"}, + }, + }, + }, + { + name: "bridge mode with interface option", + args: []string{"bridge:interface_name=eth123"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + defaultNetName: { + InterfaceName: "eth123", + }, + }, + }, + { + name: "bridge mode with invalid option", + args: []string{"bridge:abc=123"}, + nsmode: Namespace{NSMode: Bridge}, + err: "unknown bridge network option: abc", + }, + { + name: "bridge mode with invalid ip", + args: []string{"bridge:ip=10..1"}, + nsmode: Namespace{NSMode: Bridge}, + err: "invalid ip address \"10..1\"", + }, + { + name: "bridge mode with invalid mac", + args: []string{"bridge:mac=123"}, + nsmode: Namespace{NSMode: Bridge}, + err: "address 123: invalid MAC address", + }, + { + name: "network name", + args: []string{"someName"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + "someName": {}, + }, + }, + { + name: "network name with options", + args: []string{"someName:ip=10.0.0.1"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + "someName": {StaticIPs: []net.IP{net.ParseIP("10.0.0.1")}}, + }, + }, + { + name: "multiple networks", + args: []string{"someName", "net2"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + "someName": {}, + "net2": {}, + }, + }, + { + name: "multiple networks with options", + args: []string{"someName:ip=10.0.0.1", "net2:ip=10.10.0.1"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + "someName": {StaticIPs: []net.IP{net.ParseIP("10.0.0.1")}}, + "net2": {StaticIPs: []net.IP{net.ParseIP("10.10.0.1")}}, + }, + }, + { + name: "multiple networks with bridge mode first should map to default net", + args: []string{"bridge", "net2"}, + nsmode: Namespace{NSMode: Bridge}, + networks: map[string]types.PerNetworkOptions{ + defaultNetName: {}, + "net2": {}, + }, + }, + { + name: "conflicting network modes should error", + args: []string{"bridge", "host"}, + nsmode: Namespace{NSMode: Bridge}, + err: "can only set extra network names, selected mode host conflicts with bridge: invalid argument", + }, + { + name: "multiple networks empty name should error", + args: []string{"someName", ""}, + nsmode: Namespace{NSMode: Bridge}, + err: "network name cannot be empty: invalid argument", + }, + { + name: "multiple networks on invalid mode should error", + args: []string{"host", "net2"}, + nsmode: Namespace{NSMode: Host}, + err: "cannot set multiple networks without bridge network mode, selected mode host: invalid argument", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + got, got1, got2, err := ParseNetworkFlag(tt.args) + if tt.err != "" { + assert.EqualError(t, err, tt.err, tt.name) + } else { + assert.NoError(t, err, tt.name) + } + + assert.Equal(t, tt.nsmode, got, tt.name) + assert.Equal(t, tt.networks, got1, tt.name) + assert.Equal(t, tt.options, got2, tt.name) + }) + } +} diff --git a/pkg/specgen/pod_validate.go b/pkg/specgen/pod_validate.go index bca7b6dbe..c5a66189c 100644 --- a/pkg/specgen/pod_validate.go +++ b/pkg/specgen/pod_validate.go @@ -1,7 +1,6 @@ package specgen import ( - "github.com/containers/podman/v3/pkg/rootless" "github.com/containers/podman/v3/pkg/util" "github.com/pkg/errors" ) @@ -19,15 +18,6 @@ func exclusivePodOptions(opt1, opt2 string) error { // Validate verifies the input is valid func (p *PodSpecGenerator) Validate() error { - if rootless.IsRootless() && len(p.CNINetworks) == 0 { - if p.StaticIP != nil { - return ErrNoStaticIPRootless - } - if p.StaticMAC != nil { - return ErrNoStaticMACRootless - } - } - // PodBasicConfig if p.NoInfra { if len(p.InfraCommand) > 0 { @@ -52,11 +42,10 @@ func (p *PodSpecGenerator) Validate() error { if p.NetNS.NSMode != Default && p.NetNS.NSMode != "" { return errors.New("NoInfra and network modes cannot be used together") } - if p.StaticIP != nil { - return exclusivePodOptions("NoInfra", "StaticIP") - } - if p.StaticMAC != nil { - return exclusivePodOptions("NoInfra", "StaticMAC") + // Note that networks might be set when --ip or --mac was set + // so we need to check that no networks are set without the infra + if len(p.Networks) > 0 { + return errors.New("cannot set networks options without infra container") } if len(p.DNSOption) > 0 { return exclusivePodOptions("NoInfra", "DNSOption") @@ -78,10 +67,8 @@ func (p *PodSpecGenerator) Validate() error { if len(p.PortMappings) > 0 { return errors.New("PortMappings can only be used with Bridge or slirp4netns networking") } - if len(p.CNINetworks) > 0 { - return errors.New("CNINetworks can only be used with Bridge mode networking") - } } + if p.NoManageResolvConf { if len(p.DNSServer) > 0 { return exclusivePodOptions("NoManageResolvConf", "DNSServer") diff --git a/pkg/specgen/podspecgen.go b/pkg/specgen/podspecgen.go index 948fb990c..33e8422fd 100644 --- a/pkg/specgen/podspecgen.go +++ b/pkg/specgen/podspecgen.go @@ -86,33 +86,26 @@ type PodNetworkConfig struct { // Defaults to Bridge as root and Slirp as rootless. // Mandatory. NetNS Namespace `json:"netns,omitempty"` - // StaticIP sets a static IP for the infra container. As the infra - // container's network is used for the entire pod by default, this will - // thus be a static IP for the whole pod. - // Only available if NetNS is set to Bridge (the default for root). - // As such, conflicts with NoInfra=true by proxy. - // Optional. - StaticIP *net.IP `json:"static_ip,omitempty"` - // StaticMAC sets a static MAC for the infra container. As the infra - // container's network is used for the entire pod by default, this will - // thus be a static MAC for the entire pod. - // Only available if NetNS is set to Bridge (the default for root). - // As such, conflicts with NoInfra=true by proxy. - // Optional. - // swagger:strfmt string - StaticMAC *types.HardwareAddr `json:"static_mac,omitempty"` // PortMappings is a set of ports to map into the infra container. // As, by default, containers share their network with the infra // container, this will forward the ports to the entire pod. // Only available if NetNS is set to Bridge or Slirp. // Optional. PortMappings []types.PortMapping `json:"portmappings,omitempty"` - // CNINetworks is a list of CNI networks that the infra container will - // join. As, by default, containers share their network with the infra - // container, these networks will effectively be joined by the - // entire pod. - // Only available when NetNS is set to Bridge, the default for root. - // Optional. + // Map of networks names ot ids the container should join to. + // You can request additional settings for each network, you can + // set network aliases, static ips, static mac address and the + // network interface name for this container on the specifc network. + // If the map is empty and the bridge network mode is set the container + // will be joined to the default network. + Networks map[string]types.PerNetworkOptions + // CNINetworks is a list of CNI networks to join the container to. + // If this list is empty, the default CNI network will be joined + // instead. If at least one entry is present, we will not join the + // default network (unless it is part of this list). + // Only available if NetNS is set to bridge. + // Optional. + // Deprecated: as of podman 4.0 use "Networks" instead. CNINetworks []string `json:"cni_networks,omitempty"` // NoManageResolvConf indicates that /etc/resolv.conf should not be // managed by the pod. Instead, each container will create and manage a @@ -203,6 +196,7 @@ type PodSpecGenerator struct { PodCgroupConfig PodResourceConfig PodStorageConfig + PodSecurityConfig InfraContainerSpec *SpecGenerator `json:"-"` } @@ -217,6 +211,10 @@ type PodResourceConfig struct { ThrottleReadBpsDevice map[string]spec.LinuxThrottleDevice `json:"throttleReadBpsDevice,omitempty"` } +type PodSecurityConfig struct { + SecurityOpt []string `json:"security_opt,omitempty"` +} + // NewPodSpecGenerator creates a new pod spec func NewPodSpecGenerator() *PodSpecGenerator { return &PodSpecGenerator{} diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index 0e257ad4c..5989456c9 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -152,6 +152,9 @@ type ContainerBasicConfig struct { // Conflicts with UtsNS if UtsNS is not set to private. // Optional. Hostname string `json:"hostname,omitempty"` + // HostUses is a list of host usernames or UIDs to add to the container + // /etc/passwd file + HostUsers []string `json:"hostusers,omitempty"` // Sysctl sets kernel parameters for the container Sysctl map[string]string `json:"sysctl,omitempty"` // Remove indicates if the container should be removed once it has been started @@ -201,6 +204,8 @@ type ContainerBasicConfig struct { // UnsetEnvAll unsets all default environment variables from the image or from buildin // Optional. UnsetEnvAll bool `json:"unsetenvall,omitempty"` + // Passwd is a container run option that determines if we are validating users/groups before running the container + Passwd *bool `json:"manage_password,omitempty"` } // ContainerStorageConfig contains information on the storage configuration of a @@ -394,26 +399,10 @@ type ContainerCgroupConfig struct { // ContainerNetworkConfig contains information on a container's network // configuration. type ContainerNetworkConfig struct { - // Aliases are a list of network-scoped aliases for container - // Optional - Aliases map[string][]string `json:"aliases"` // NetNS is the configuration to use for the container's network // namespace. // Mandatory. NetNS Namespace `json:"netns,omitempty"` - // StaticIP is the a IPv4 address of the container. - // Only available if NetNS is set to Bridge. - // Optional. - StaticIP *net.IP `json:"static_ip,omitempty"` - // StaticIPv6 is a static IPv6 address to set in the container. - // Only available if NetNS is set to Bridge. - // Optional. - StaticIPv6 *net.IP `json:"static_ipv6,omitempty"` - // StaticMAC is a static MAC address to set in the container. - // Only available if NetNS is set to bridge. - // Optional. - // swagger:strfmt string - StaticMAC *nettypes.HardwareAddr `json:"static_mac,omitempty"` // PortBindings is a set of ports to map into the container. // Only available if NetNS is set to bridge or slirp. // Optional. @@ -434,12 +423,20 @@ type ContainerNetworkConfig struct { // PublishExposedPorts is set. // Optional. Expose map[uint16]string `json:"expose,omitempty"` + // Map of networks names ot ids the container should join to. + // You can request additional settings for each network, you can + // set network aliases, static ips, static mac address and the + // network interface name for this container on the specifc network. + // If the map is empty and the bridge network mode is set the container + // will be joined to the default network. + Networks map[string]nettypes.PerNetworkOptions // CNINetworks is a list of CNI networks to join the container to. // If this list is empty, the default CNI network will be joined // instead. If at least one entry is present, we will not join the // default network (unless it is part of this list). // Only available if NetNS is set to bridge. // Optional. + // Deprecated: as of podman 4.0 use "Networks" instead. CNINetworks []string `json:"cni_networks,omitempty"` // UseImageResolvConf indicates that resolv.conf should not be managed // by Podman, but instead sourced from the image. diff --git a/pkg/specgenutil/specgen.go b/pkg/specgenutil/specgen.go index 5e4bd2f65..8e43cc50e 100644 --- a/pkg/specgenutil/specgen.go +++ b/pkg/specgenutil/specgen.go @@ -11,7 +11,6 @@ import ( "github.com/containers/image/v5/manifest" "github.com/containers/podman/v3/cmd/podman/parse" "github.com/containers/podman/v3/libpod/define" - "github.com/containers/podman/v3/libpod/network/types" ann "github.com/containers/podman/v3/pkg/annotations" "github.com/containers/podman/v3/pkg/domain/entities" envLib "github.com/containers/podman/v3/pkg/env" @@ -164,14 +163,6 @@ func getMemoryLimits(s *specgen.SpecGenerator, c *entities.ContainerCreateOption hasLimits = true } } - if m := c.KernelMemory; len(m) > 0 { - mk, err := units.RAMInBytes(m) - if err != nil { - return nil, errors.Wrapf(err, "invalid value for kernel-memory") - } - memory.Kernel = &mk - hasLimits = true - } if c.MemorySwappiness >= 0 { swappiness := uint64(c.MemorySwappiness) memory.Swappiness = &swappiness @@ -434,19 +425,7 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions } if c.Net != nil { - s.CNINetworks = c.Net.CNINetworks - } - - // Network aliases - if c.Net != nil { - if len(c.Net.Aliases) > 0 { - // build a map of aliases where key=cniName - aliases := make(map[string][]string, len(s.CNINetworks)) - for _, cniNetwork := range s.CNINetworks { - aliases[cniNetwork] = c.Net.Aliases - } - s.Aliases = aliases - } + s.Networks = c.Net.Networks } if c.Net != nil { @@ -455,12 +434,10 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions s.DNSServers = c.Net.DNSServers s.DNSSearch = c.Net.DNSSearch s.DNSOptions = c.Net.DNSOptions - s.StaticIP = c.Net.StaticIP - // type cast to types.HardwareAddr - s.StaticMAC = (*types.HardwareAddr)(c.Net.StaticMAC) s.NetworkOptions = c.Net.NetworkOptions s.UseImageHosts = c.Net.NoHosts } + s.HostUsers = c.HostUsers s.ImageVolumeMode = c.ImageVolume if s.ImageVolumeMode == "bind" { s.ImageVolumeMode = "anonymous" @@ -714,6 +691,9 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions // Initcontainers s.InitContainerType = c.InitContainerType + + t := true + s.Passwd = &t return nil } diff --git a/pkg/util/utils.go b/pkg/util/utils.go index 208d815d9..11edf265f 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -665,8 +665,8 @@ func CreateCidFile(cidfile string, id string) error { return nil } -// DefaultCPUPeriod is the default CPU period is 100us, which is the same default -// as Kubernetes. +// DefaultCPUPeriod is the default CPU period (100ms) in microseconds, which is +// the same default as Kubernetes. const DefaultCPUPeriod uint64 = 100000 // CoresToPeriodAndQuota converts a fraction of cores to the equivalent @@ -723,3 +723,11 @@ func SocketPath() (string, error) { // Glue the socket path together return filepath.Join(xdg, "podman", "podman.sock"), nil } + +func LookupUser(name string) (*user.User, error) { + // Assume UID look up first, if it fails lookup by username + if u, err := user.LookupId(name); err == nil { + return u, err + } + return user.Lookup(name) +} |