diff options
48 files changed, 596 insertions, 168 deletions
@@ -73,9 +73,9 @@ A little configuration by an administrator is required before rootless Podman ca standard for composing Pods and for orchestrating containers, making Kubernetes YAML a defacto standard file format. Hence, Podman allows the creation and execution of Pods from a Kubernetes YAML file (see - [podman-play-kube](https://github.com/containers/libpod/blob/master/docs/podman-play-kube.1.md)). + [podman-play-kube](https://github.com/containers/libpod/blob/master/docs/source/markdown/podman-play-kube.1.md)). Podman can also generate Kubernetes YAML based on a container or Pod (see - [podman-generate-kube](https://github.com/containers/libpod/blob/master/docs/podman-generate-kube.1.md)), + [podman-generate-kube](https://github.com/containers/libpod/blob/master/docs/source/markdown/podman-generate-kube.1.md)), which allows for an easy transition from a local development environment to a production Kubernetes cluster. If Kubernetes does not fit your requirements, there are other third-party tools that support the docker-compose format such as diff --git a/cmd/podman/common.go b/cmd/podman/common.go index 7610edbc0..6fa2b3c71 100644 --- a/cmd/podman/common.go +++ b/cmd/podman/common.go @@ -257,6 +257,10 @@ func getCreateFlags(c *cliconfig.PodmanCommand) { "Add a host device to the container (default [])", ) createFlags.StringSlice( + "device-cgroup-rule", []string{}, + "Add a rule to the cgroup allowed devices list", + ) + createFlags.StringSlice( "device-read-bps", []string{}, "Limit read rate (bytes per second) from a device (e.g. --device-read-bps=/dev/sda:1mb)", ) diff --git a/cmd/podman/images.go b/cmd/podman/images.go index 115f30d9b..de61690ae 100644 --- a/cmd/podman/images.go +++ b/cmd/podman/images.go @@ -21,16 +21,16 @@ import ( ) type imagesTemplateParams struct { - Repository string - Tag string - ID string - Digest digest.Digest - Digests []digest.Digest - Created string - CreatedTime time.Time - Size string - ReadOnly bool - History string + Repository string + Tag string + ID string + Digest digest.Digest + Digests []digest.Digest + CreatedAt time.Time + CreatedSince string + Size string + ReadOnly bool + History string } type imagesJSONParams struct { @@ -65,7 +65,7 @@ func (a imagesSorted) Swap(i, j int) { a[i], a[j] = a[j], a[i] } type imagesSortedCreated struct{ imagesSorted } func (a imagesSortedCreated) Less(i, j int) bool { - return a.imagesSorted[i].CreatedTime.After(a.imagesSorted[j].CreatedTime) + return a.imagesSorted[i].CreatedAt.After(a.imagesSorted[j].CreatedAt) } type imagesSortedID struct{ imagesSorted } @@ -185,7 +185,17 @@ func imagesCmd(c *cliconfig.ImagesValues) error { history: c.History, } - opts.outputformat = opts.setOutputFormat() + outputformat := opts.setOutputFormat() + // These fields were renamed, so we need to provide backward compat for + // the old names. + if strings.Contains(outputformat, "{{.Created}}") { + outputformat = strings.Replace(outputformat, "{{.Created}}", "{{.CreatedSince}}", -1) + } + if strings.Contains(outputformat, "{{.CreatedTime}}") { + outputformat = strings.Replace(outputformat, "{{.CreatedTime}}", "{{.CreatedAt}}", -1) + } + opts.outputformat = outputformat + filteredImages, err := runtime.GetFilteredImages(filters, false) if err != nil { return errors.Wrapf(err, "unable to get images") @@ -216,7 +226,7 @@ func (i imagesOptions) setOutputFormat() string { if i.digests { format += "{{.Digest}}\t" } - format += "{{.ID}}\t{{.Created}}\t{{.Size}}\t" + format += "{{.ID}}\t{{.CreatedSince}}\t{{.Size}}\t" if i.history { format += "{{if .History}}{{.History}}{{else}}<none>{{end}}\t" } @@ -301,16 +311,16 @@ func getImagesTemplateOutput(ctx context.Context, images []*adapter.ContainerIma imageDigest = img.Digest() } params := imagesTemplateParams{ - Repository: repo, - Tag: tag, - ID: imageID, - Digest: imageDigest, - Digests: img.Digests(), - CreatedTime: createdTime, - Created: units.HumanDuration(time.Since(createdTime)) + " ago", - Size: sizeStr, - ReadOnly: img.IsReadOnly(), - History: strings.Join(img.NamesHistory(), ", "), + Repository: repo, + Tag: tag, + ID: imageID, + Digest: imageDigest, + Digests: img.Digests(), + CreatedAt: createdTime, + CreatedSince: units.HumanDuration(time.Since(createdTime)) + " ago", + Size: sizeStr, + ReadOnly: img.IsReadOnly(), + History: strings.Join(img.NamesHistory(), ", "), } imagesOutput = append(imagesOutput, params) if opts.quiet { // Show only one image ID when quiet @@ -384,6 +394,9 @@ func GenImageOutputMap() map[string]string { values[key] = "R/O" continue } + if value == "CreatedSince" { + value = "created" + } values[key] = strings.ToUpper(splitCamelCase(value)) } return values diff --git a/cmd/podman/ps.go b/cmd/podman/ps.go index d93ccc24c..accd5b51a 100644 --- a/cmd/podman/ps.go +++ b/cmd/podman/ps.go @@ -205,9 +205,15 @@ func checkFlagsPassed(c *cliconfig.PsValues) error { if c.Last >= 0 && c.Latest { return errors.Errorf("last and latest are mutually exclusive") } - // Filter forces all + // Filter on status forces all if len(c.Filter) > 0 { - c.All = true + for _, filter := range c.Filter { + splitFilter := strings.SplitN(filter, "=", 2) + if strings.ToLower(splitFilter[0]) == "status" { + c.All = true + break + } + } } // Quiet conflicts with size and namespace and is overridden by a Go // template. diff --git a/cmd/podman/shared/create.go b/cmd/podman/shared/create.go index e3a39a421..be5adcccb 100644 --- a/cmd/podman/shared/create.go +++ b/cmd/podman/shared/create.go @@ -758,6 +758,7 @@ func ParseCreateOpts(ctx context.Context, c *GenericCLIResults, runtime *libpod. CPURtPeriod: c.Uint64("cpu-rt-period"), CPURtRuntime: c.Int64("cpu-rt-runtime"), CPUs: c.Float64("cpus"), + DeviceCgroupRules: c.StringSlice("device-cgroup-rule"), DeviceReadBps: c.StringSlice("device-read-bps"), DeviceReadIOps: c.StringSlice("device-read-iops"), DeviceWriteBps: c.StringSlice("device-write-bps"), diff --git a/cmd/podman/shared/intermediate.go b/cmd/podman/shared/intermediate.go index cfb3f612c..ee212234f 100644 --- a/cmd/podman/shared/intermediate.go +++ b/cmd/podman/shared/intermediate.go @@ -386,6 +386,7 @@ func NewIntermediateLayer(c *cliconfig.PodmanCommand, remote bool) GenericCLIRes m["detach"] = newCRBool(c, "detach") m["detach-keys"] = newCRString(c, "detach-keys") m["device"] = newCRStringSlice(c, "device") + m["device-cgroup-rule"] = newCRStringSlice(c, "device-cgroup-rule") m["device-read-bps"] = newCRStringSlice(c, "device-read-bps") m["device-read-iops"] = newCRStringSlice(c, "device-read-iops") m["device-write-bps"] = newCRStringSlice(c, "device-write-bps") diff --git a/completions/bash/podman b/completions/bash/podman index 99e6a3bcb..d1dcef0a4 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -1873,6 +1873,7 @@ _podman_container_run() { --cpuset-mems --cpu-shares -c --device + --device-cgroup-rule --device-read-bps --device-read-iops --device-write-bps diff --git a/docs/source/markdown/podman-create.1.md b/docs/source/markdown/podman-create.1.md index 977382e61..ca38be6a1 100644 --- a/docs/source/markdown/podman-create.1.md +++ b/docs/source/markdown/podman-create.1.md @@ -209,6 +209,13 @@ Note: if the user only has access rights via a group then accessing the device from inside a rootless container will fail. The `crun` runtime offers a workaround for this by adding the option `--annotation run.oci.keep_original_groups=1`. +**--device-cgroup-rule**="type major:minor mode" + +Add a rule to the cgroup allowed devices list. The rule is expected to be in the format specified in the Linux kernel documentation (Documentation/cgroup-v1/devices.txt): + - type: a (all), c (char), or b (block); + - major and minor: either a number, or * for all; + - mode: a composition of r (read), w (write), and m (mknod(2)). + **--device-read-bps**=*path* Limit read rate (bytes per second) from a device (e.g. --device-read-bps=/dev/sda:1mb) diff --git a/docs/source/markdown/podman-images.1.md b/docs/source/markdown/podman-images.1.md index 21fca1dbd..d22fb940f 100644 --- a/docs/source/markdown/podman-images.1.md +++ b/docs/source/markdown/podman-images.1.md @@ -51,6 +51,18 @@ Filter output based on conditions provided Change the default output format. This can be of a supported type like 'json' or a Go template. +Valid placeholders for the Go template are listed below: + +| **Placeholder** | **Description** | +| --------------- | ----------------------------------------------------------------------------- | +| .ID | Image ID | +| .Repository | Image repository | +| .Tag | Image tag | +| .Digest | Image digest | +| .CreatedSince | Elapsed time since the image was created | +| .CreatedAt | Time when the image was created | +| .Size | Size of layer on disk | +| .History | History of the image layer | **--history** @@ -38,7 +38,7 @@ require ( github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf github.com/google/uuid v1.1.1 github.com/gorilla/handlers v1.4.2 // indirect - github.com/gorilla/mux v1.7.3 + github.com/gorilla/mux v1.7.4 github.com/gorilla/schema v1.1.0 github.com/hashicorp/go-multierror v1.0.0 github.com/hpcloud/tail v1.0.0 @@ -77,8 +77,8 @@ require ( google.golang.org/appengine v1.6.1 // indirect google.golang.org/genproto v0.0.0-20190620144150-6af8c5fc6601 // indirect gopkg.in/yaml.v2 v2.2.8 - k8s.io/api v0.17.2 - k8s.io/apimachinery v0.17.2 + k8s.io/api v0.17.3 + k8s.io/apimachinery v0.17.3 k8s.io/client-go v0.0.0-20190620085101-78d2af792bab k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a // indirect ) @@ -213,6 +213,8 @@ github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/ github.com/gorilla/mux v0.0.0-20170217192616-94e7d24fd285/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY= github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= @@ -621,10 +623,14 @@ k8s.io/api v0.0.0-20190620084959-7cf5895f2711/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E k8s.io/api v0.17.0/go.mod h1:npsyOePkeP0CPwyGfXDHxvypiYMJxBWAMpQxCaJ4ZxI= k8s.io/api v0.17.2 h1:NF1UFXcKN7/OOv1uxdRz3qfra8AHsPav5M93hlV9+Dc= k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4= +k8s.io/api v0.17.3 h1:XAm3PZp3wnEdzekNkcmj/9Y1zdmQYJ1I4GKSBBZ8aG0= +k8s.io/api v0.17.3/go.mod h1:YZ0OTkuw7ipbe305fMpIdf3GLXZKRigjtZaV5gzC2J0= k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA= k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= k8s.io/apimachinery v0.17.2 h1:hwDQQFbdRlpnnsR64Asdi55GyCaIP/3WQpMmbNBeWr4= k8s.io/apimachinery v0.17.2/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/apimachinery v0.17.3 h1:f+uZV6rm4/tHE7xXgLyToprg6xWairaClGVkm2t8omg= +k8s.io/apimachinery v0.17.3/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g= k8s.io/client-go v0.0.0-20170217214107-bcde30fb7eae/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= k8s.io/client-go v0.0.0-20181219152756-3dd551c0f083/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= k8s.io/client-go v0.0.0-20190620085101-78d2af792bab h1:E8Fecph0qbNsAbijJJQryKu4Oi9QTp5cVpjTE+nqg6g= diff --git a/libpod/image/image_test.go b/libpod/image/image_test.go index 3ff6210d9..19f7eee1e 100644 --- a/libpod/image/image_test.go +++ b/libpod/image/image_test.go @@ -18,7 +18,6 @@ import ( var ( bbNames = []string{"docker.io/library/busybox:latest", "docker.io/library/busybox", "docker.io/busybox:latest", "docker.io/busybox", "busybox:latest", "busybox"} bbGlibcNames = []string{"docker.io/library/busybox:glibc", "docker.io/busybox:glibc", "busybox:glibc"} - fedoraNames = []string{"registry.fedoraproject.org/fedora-minimal:latest", "registry.fedoraproject.org/fedora-minimal", "fedora-minimal:latest", "fedora-minimal"} ) type localImageTest struct { @@ -139,7 +138,6 @@ func TestImage_New(t *testing.T) { ir.Eventer = events.NewNullEventer() // Build the list of pull names names = append(names, bbNames...) - names = append(names, fedoraNames...) writer := os.Stdout // Iterate over the names and delete the image diff --git a/libpod/stats.go b/libpod/stats.go index 3b5e0958c..6f42afd18 100644 --- a/libpod/stats.go +++ b/libpod/stats.go @@ -66,7 +66,9 @@ func (c *Container) GetContainerStats(previousStats *ContainerStats) (*Container } stats.BlockInput, stats.BlockOutput = calculateBlockIO(cgroupStats) stats.CPUNano = cgroupStats.CPU.Usage.Total + stats.CPUSystemNano = cgroupStats.CPU.Usage.Kernel stats.SystemNano = now + stats.PerCPU = cgroupStats.CPU.Usage.PerCPU // Handle case where the container is not in a network namespace if netStats != nil { stats.NetInput = netStats.TxBytes diff --git a/libpod/stats_config.go b/libpod/stats_config.go index 9c7d97298..91d3d1493 100644 --- a/libpod/stats_config.go +++ b/libpod/stats_config.go @@ -2,17 +2,19 @@ package libpod // ContainerStats contains the statistics information for a running container type ContainerStats struct { - ContainerID string - Name string - CPU float64 - CPUNano uint64 - SystemNano uint64 - MemUsage uint64 - MemLimit uint64 - MemPerc float64 - NetInput uint64 - NetOutput uint64 - BlockInput uint64 - BlockOutput uint64 - PIDs uint64 + ContainerID string + Name string + PerCPU []uint64 + CPU float64 + CPUNano uint64 + CPUSystemNano uint64 + SystemNano uint64 + MemUsage uint64 + MemLimit uint64 + MemPerc float64 + NetInput uint64 + NetOutput uint64 + BlockInput uint64 + BlockOutput uint64 + PIDs uint64 } diff --git a/pkg/adapter/pods.go b/pkg/adapter/pods.go index b0e63f770..a30ec6649 100644 --- a/pkg/adapter/pods.go +++ b/pkg/adapter/pods.go @@ -764,7 +764,6 @@ func kubeContainerToCreateConfig(ctx context.Context, containerYAML v1.Container containerConfig.ImageID = newImage.ID() containerConfig.Name = containerYAML.Name containerConfig.Tty = containerYAML.TTY - containerConfig.WorkDir = containerYAML.WorkingDir containerConfig.Pod = podID @@ -796,6 +795,27 @@ func kubeContainerToCreateConfig(ctx context.Context, containerYAML v1.Container containerConfig.StopSignal = 15 + containerConfig.WorkDir = "/" + if imageData != nil { + // FIXME, + // we are currently ignoring imageData.Config.ExposedPorts + containerConfig.BuiltinImgVolumes = imageData.Config.Volumes + if imageData.Config.WorkingDir != "" { + containerConfig.WorkDir = imageData.Config.WorkingDir + } + containerConfig.Labels = imageData.Config.Labels + if imageData.Config.StopSignal != "" { + stopSignal, err := util.ParseSignal(imageData.Config.StopSignal) + if err != nil { + return nil, err + } + containerConfig.StopSignal = stopSignal + } + } + + if containerYAML.WorkingDir != "" { + containerConfig.WorkDir = containerYAML.WorkingDir + } // If the user does not pass in ID mappings, just set to basics if userConfig.IDMappings == nil { userConfig.IDMappings = &storage.IDMappingOptions{} diff --git a/pkg/api/handlers/generic/containers_stats.go b/pkg/api/handlers/generic/containers_stats.go index cbc1be2f0..19e2cc882 100644 --- a/pkg/api/handlers/generic/containers_stats.go +++ b/pkg/api/handlers/generic/containers_stats.go @@ -61,14 +61,15 @@ func StatsContainer(w http.ResponseWriter, r *http.Request) { var preCPUStats docker.CPUStats if query.Stream { preRead = time.Now() + systemUsage, _ := cgroups.GetSystemCPUUsage() preCPUStats = docker.CPUStats{ CPUUsage: docker.CPUUsage{ TotalUsage: stats.CPUNano, - PercpuUsage: []uint64{uint64(stats.CPU)}, - UsageInKernelmode: 0, - UsageInUsermode: 0, + PercpuUsage: stats.PerCPU, + UsageInKernelmode: stats.CPUSystemNano, + UsageInUsermode: stats.CPUNano - stats.CPUSystemNano, }, - SystemUsage: 0, + SystemUsage: systemUsage, OnlineCPUs: 0, ThrottlingData: docker.ThrottlingData{}, } @@ -122,6 +123,8 @@ func StatsContainer(w http.ResponseWriter, r *http.Request) { InstanceID: "", } + systemUsage, _ := cgroups.GetSystemCPUUsage() + s := handlers.Stats{StatsJSON: docker.StatsJSON{ Stats: docker.Stats{ Read: time.Now(), @@ -143,11 +146,11 @@ func StatsContainer(w http.ResponseWriter, r *http.Request) { CPUStats: docker.CPUStats{ CPUUsage: docker.CPUUsage{ TotalUsage: cgroupStat.CPU.Usage.Total, - PercpuUsage: []uint64{uint64(stats.CPU)}, + PercpuUsage: cgroupStat.CPU.Usage.PerCPU, UsageInKernelmode: cgroupStat.CPU.Usage.Kernel, UsageInUsermode: cgroupStat.CPU.Usage.Total - cgroupStat.CPU.Usage.Kernel, }, - SystemUsage: 0, + SystemUsage: systemUsage, OnlineCPUs: uint32(len(cgroupStat.CPU.Usage.PerCPU)), ThrottlingData: docker.ThrottlingData{ Periods: 0, diff --git a/pkg/api/handlers/libpod/containers.go b/pkg/api/handlers/libpod/containers.go index a64ed446c..ce2836a72 100644 --- a/pkg/api/handlers/libpod/containers.go +++ b/pkg/api/handlers/libpod/containers.go @@ -56,7 +56,7 @@ func ListContainers(w http.ResponseWriter, r *http.Request) { decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { All bool `schema:"all"` - Filter map[string][]string `schema:"filter"` + Filters map[string][]string `schema:"filters"` Last int `schema:"last"` Namespace bool `schema:"namespace"` Pod bool `schema:"pod"` @@ -71,6 +71,7 @@ func ListContainers(w http.ResponseWriter, r *http.Request) { errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) return } + runtime := r.Context().Value("runtime").(*libpod.Runtime) opts := shared.PsOptions{ All: query.All, @@ -82,8 +83,10 @@ func ListContainers(w http.ResponseWriter, r *http.Request) { Pod: query.Pod, Sync: query.Sync, } - if len(query.Filter) > 0 { - for k, v := range query.Filter { + + all := query.All + if len(query.Filters) > 0 { + for k, v := range query.Filters { for _, val := range v { generatedFunc, err := shared.GenerateContainerFilterFuncs(k, val, runtime) if err != nil { @@ -95,8 +98,12 @@ func ListContainers(w http.ResponseWriter, r *http.Request) { } } - if !query.All { - // The default is get only running containers. Do this with a filterfunc + // Docker thinks that if status is given as an input, then we should override + // the all setting and always deal with all containers. + if len(query.Filters["status"]) > 0 { + all = true + } + if !all { runningOnly, err := shared.GenerateContainerFilterFuncs("status", define.ContainerStateRunning.String(), runtime) if err != nil { utils.InternalServerError(w, err) diff --git a/pkg/api/server/register_swagger.go b/pkg/api/server/register_swagger.go new file mode 100644 index 000000000..5564ec096 --- /dev/null +++ b/pkg/api/server/register_swagger.go @@ -0,0 +1,26 @@ +package server + +import ( + "net/http" + "os" + + "github.com/gorilla/mux" +) + +// DefaultPodmanSwaggerSpec provides the default path to the podman swagger spec file +const DefaultPodmanSwaggerSpec = "/usr/share/containers/podman/swagger.yaml" + +// RegisterSwaggerHandlers maps the swagger endpoint for the server +func (s *APIServer) RegisterSwaggerHandlers(r *mux.Router) error { + // This handler does _*NOT*_ provide an UI rather just a swagger spec that an UI could render + r.PathPrefix("/swagger/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := DefaultPodmanSwaggerSpec + if p, found := os.LookupEnv("PODMAN_SWAGGER_SPEC"); found { + path = p + } + w.Header().Set("Content-Type", "text/yaml") + + http.ServeFile(w, r, path) + }) + return nil +} diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index 87b11b716..2c709bc59 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -114,6 +114,7 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li server.registerPingHandlers, server.RegisterPluginsHandlers, server.registerPodsHandlers, + server.RegisterSwaggerHandlers, server.RegisterSwarmHandlers, server.registerSystemHandlers, server.registerVersionHandlers, diff --git a/pkg/bindings/connection.go b/pkg/bindings/connection.go index 116af9709..f270060a6 100644 --- a/pkg/bindings/connection.go +++ b/pkg/bindings/connection.go @@ -130,7 +130,7 @@ func (c *Connection) DoRequest(httpBody io.Reader, httpMethod, endpoint string, // if more desirable we could use url to form the encoded endpoint with params r := req.URL.Query() for k, v := range queryParams { - r.Add(k, url.QueryEscape(v)) + r.Add(k, v) } req.URL.RawQuery = r.Encode() } @@ -155,18 +155,14 @@ func GetConnectionFromContext(ctx context.Context) (*Connection, error) { return conn, nil } -// FiltersToHTML converts our typical filter format of a +// FiltersToString converts our typical filter format of a // map[string][]string to a query/html safe string. -func FiltersToHTML(filters map[string][]string) (string, error) { +func FiltersToString(filters map[string][]string) (string, error) { lowerCaseKeys := make(map[string][]string) for k, v := range filters { lowerCaseKeys[strings.ToLower(k)] = v } - unsafeString, err := jsoniter.MarshalToString(lowerCaseKeys) - if err != nil { - return "", err - } - return url.QueryEscape(unsafeString), nil + return jsoniter.MarshalToString(lowerCaseKeys) } // IsInformation returns true if the response code is 1xx diff --git a/pkg/bindings/containers/containers.go b/pkg/bindings/containers/containers.go index 334a656d4..04f7f8802 100644 --- a/pkg/bindings/containers/containers.go +++ b/pkg/bindings/containers/containers.go @@ -5,8 +5,8 @@ import ( "net/http" "strconv" - "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" + lpapiv2 "github.com/containers/libpod/pkg/api/handlers/libpod" "github.com/containers/libpod/pkg/bindings" ) @@ -15,13 +15,16 @@ import ( // the most recent number of containers. The pod and size booleans indicate that pod information and rootfs // size information should also be included. Finally, the sync bool synchronizes the OCI runtime and // container state. -func List(ctx context.Context, filters map[string][]string, last *int, pod, size, sync *bool) ([]*shared.PsContainerOutput, error) { // nolint:typecheck +func List(ctx context.Context, filters map[string][]string, all *bool, last *int, pod, size, sync *bool) ([]lpapiv2.ListContainer, error) { // nolint:typecheck conn, err := bindings.GetConnectionFromContext(ctx) if err != nil { return nil, err } - var images []*shared.PsContainerOutput + var containers []lpapiv2.ListContainer params := make(map[string]string) + if all != nil { + params["all"] = strconv.FormatBool(*all) + } if last != nil { params["last"] = strconv.Itoa(*last) } @@ -35,7 +38,7 @@ func List(ctx context.Context, filters map[string][]string, last *int, pod, size params["sync"] = strconv.FormatBool(*sync) } if filters != nil { - filterString, err := bindings.FiltersToHTML(filters) + filterString, err := bindings.FiltersToString(filters) if err != nil { return nil, err } @@ -43,9 +46,9 @@ func List(ctx context.Context, filters map[string][]string, last *int, pod, size } response, err := conn.DoRequest(nil, http.MethodGet, "/containers/json", params) if err != nil { - return images, err + return containers, err } - return images, response.Process(nil) + return containers, response.Process(&containers) } // Prune removes stopped and exited containers from local storage. The optional filters can be @@ -62,7 +65,7 @@ func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { } params := make(map[string]string) if filters != nil { - filterString, err := bindings.FiltersToHTML(filters) + filterString, err := bindings.FiltersToString(filters) if err != nil { return nil, err } diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go index deaf93f0e..b19482943 100644 --- a/pkg/bindings/images/images.go +++ b/pkg/bindings/images/images.go @@ -38,7 +38,7 @@ func List(ctx context.Context, all *bool, filters map[string][]string) ([]*handl params["all"] = strconv.FormatBool(*all) } if filters != nil { - strFilters, err := bindings.FiltersToHTML(filters) + strFilters, err := bindings.FiltersToString(filters) if err != nil { return nil, err } @@ -155,7 +155,7 @@ func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { } params := make(map[string]string) if filters != nil { - stringFilter, err := bindings.FiltersToHTML(filters) + stringFilter, err := bindings.FiltersToString(filters) if err != nil { return nil, err } diff --git a/pkg/bindings/images/search.go b/pkg/bindings/images/search.go index d98ddf18d..58b25425b 100644 --- a/pkg/bindings/images/search.go +++ b/pkg/bindings/images/search.go @@ -26,7 +26,7 @@ func Search(ctx context.Context, term string, limit *int, filters map[string][]s params["limit"] = strconv.Itoa(*limit) } if filters != nil { - stringFilter, err := bindings.FiltersToHTML(filters) + stringFilter, err := bindings.FiltersToString(filters) if err != nil { return nil, err } diff --git a/pkg/bindings/pods/pods.go b/pkg/bindings/pods/pods.go index a6b74c21d..d079f01c2 100644 --- a/pkg/bindings/pods/pods.go +++ b/pkg/bindings/pods/pods.go @@ -97,7 +97,7 @@ func List(ctx context.Context, filters map[string][]string) (*[]libpod.PodInspec } params := make(map[string]string) if filters != nil { - stringFilter, err := bindings.FiltersToHTML(filters) + stringFilter, err := bindings.FiltersToString(filters) if err != nil { return nil, err } diff --git a/pkg/cgroups/cgroups.go b/pkg/cgroups/cgroups.go index 96786223d..6c5b7978c 100644 --- a/pkg/cgroups/cgroups.go +++ b/pkg/cgroups/cgroups.go @@ -536,15 +536,14 @@ func (c *CgroupControl) Stat() (*Metrics, error) { return &m, nil } -func readCgroup2MapFile(ctr *CgroupControl, name string) (map[string][]string, error) { +func readCgroup2MapPath(path string) (map[string][]string, error) { ret := map[string][]string{} - p := filepath.Join(cgroupRoot, ctr.path, name) - f, err := os.Open(p) + f, err := os.Open(path) if err != nil { if os.IsNotExist(err) { return ret, nil } - return nil, errors.Wrapf(err, "open file %s", p) + return nil, errors.Wrapf(err, "open file %s", path) } defer f.Close() scanner := bufio.NewScanner(f) @@ -557,7 +556,13 @@ func readCgroup2MapFile(ctr *CgroupControl, name string) (map[string][]string, e ret[parts[0]] = parts[1:] } if err := scanner.Err(); err != nil { - return nil, errors.Wrapf(err, "parsing file %s", p) + return nil, errors.Wrapf(err, "parsing file %s", path) } return ret, nil } + +func readCgroup2MapFile(ctr *CgroupControl, name string) (map[string][]string, error) { + p := filepath.Join(cgroupRoot, ctr.path, name) + + return readCgroup2MapPath(p) +} diff --git a/pkg/cgroups/cpu.go b/pkg/cgroups/cpu.go index a43a76b22..5f0a18031 100644 --- a/pkg/cgroups/cpu.go +++ b/pkg/cgroups/cpu.go @@ -121,3 +121,42 @@ func (c *cpuHandler) Stat(ctr *CgroupControl, m *Metrics) error { m.CPU = CPUMetrics{Usage: usage} return nil } + +// GetSystemCPUUsage returns the system usage for all the cgroups +func GetSystemCPUUsage() (uint64, error) { + cgroupv2, err := IsCgroup2UnifiedMode() + if err != nil { + return 0, err + } + if !cgroupv2 { + p := filepath.Join(cgroupRoot, CPUAcct, "cpuacct.usage") + return readFileAsUint64(p) + } + + files, err := ioutil.ReadDir(cgroupRoot) + if err != nil { + return 0, errors.Wrapf(err, "read directory %q", cgroupRoot) + } + var total uint64 + for _, file := range files { + if !file.IsDir() { + continue + } + p := filepath.Join(cgroupRoot, file.Name(), "cpu.stat") + + values, err := readCgroup2MapPath(p) + if err != nil { + return 0, err + } + + if val, found := values["usage_usec"]; found { + v, err := strconv.ParseUint(cleanString(val[0]), 10, 0) + if err != nil { + return 0, err + } + total += v * 1000 + } + + } + return total, nil +} diff --git a/pkg/rootlessport/rootlessport_linux.go b/pkg/rootlessport/rootlessport_linux.go index 3e678d33a..2b51f4e09 100644 --- a/pkg/rootlessport/rootlessport_linux.go +++ b/pkg/rootlessport/rootlessport_linux.go @@ -122,6 +122,7 @@ func parent() error { logrus.WithError(driverErr).Warn("parent driver exited") } errCh <- driverErr + close(errCh) }() opaque := driver.OpaqueForChild() logrus.Infof("opaque=%+v", opaque) @@ -142,15 +143,12 @@ func parent() error { }() // reexec the child process in the child netns - cmd := exec.Command(fmt.Sprintf("/proc/%d/exe", os.Getpid())) + cmd := exec.Command("/proc/self/exe") cmd.Args = []string{reexecChildKey} cmd.Stdin = childQuitR cmd.Stdout = &logrusWriter{prefix: "child"} cmd.Stderr = cmd.Stdout cmd.Env = append(os.Environ(), reexecChildEnvOpaque+"="+string(opaqueJSON)) - cmd.SysProcAttr = &syscall.SysProcAttr{ - Pdeathsig: syscall.SIGTERM, - } childNS, err := ns.GetNS(cfg.NetNSPath) if err != nil { return err @@ -162,14 +160,27 @@ func parent() error { return err } + defer func() { + if err := syscall.Kill(cmd.Process.Pid, syscall.SIGTERM); err != nil { + logrus.WithError(err).Warn("kill child process") + } + }() + logrus.Info("waiting for initComplete") // wait for the child to connect to the parent - select { - case <-initComplete: - logrus.Infof("initComplete is closed; parent and child established the communication channel") - case err := <-errCh: - return err +outer: + for { + select { + case <-initComplete: + logrus.Infof("initComplete is closed; parent and child established the communication channel") + break outer + case err := <-errCh: + if err != nil { + return err + } + } } + defer func() { logrus.Info("stopping parent driver") quit <- struct{}{} diff --git a/pkg/spec/config_linux.go b/pkg/spec/config_linux.go index 32d8cb4de..5f39b6d0d 100644 --- a/pkg/spec/config_linux.go +++ b/pkg/spec/config_linux.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strconv" "strings" "github.com/containers/libpod/pkg/rootless" @@ -90,6 +91,42 @@ func devicesFromPath(g *generate.Generator, devicePath string) error { return addDevice(g, strings.Join(append([]string{resolvedDevicePath}, devs[1:]...), ":")) } +func deviceCgroupRules(g *generate.Generator, deviceCgroupRules []string) error { + for _, deviceCgroupRule := range deviceCgroupRules { + if err := validateDeviceCgroupRule(deviceCgroupRule); err != nil { + return err + } + ss := parseDeviceCgroupRule(deviceCgroupRule) + if len(ss[0]) != 5 { + return errors.Errorf("invalid device cgroup rule format: '%s'", deviceCgroupRule) + } + matches := ss[0] + var major, minor *int64 + if matches[2] == "*" { + majorDev := int64(-1) + major = &majorDev + } else { + majorDev, err := strconv.ParseInt(matches[2], 10, 64) + if err != nil { + return errors.Errorf("invalid major value in device cgroup rule format: '%s'", deviceCgroupRule) + } + major = &majorDev + } + if matches[3] == "*" { + minorDev := int64(-1) + minor = &minorDev + } else { + minorDev, err := strconv.ParseInt(matches[2], 10, 64) + if err != nil { + return errors.Errorf("invalid major value in device cgroup rule format: '%s'", deviceCgroupRule) + } + minor = &minorDev + } + g.AddLinuxResourcesDevice(true, matches[1], major, minor, matches[4]) + } + return nil +} + func addDevice(g *generate.Generator, device string) error { src, dst, permissions, err := ParseDevice(device) if err != nil { diff --git a/pkg/spec/config_unsupported.go b/pkg/spec/config_unsupported.go index a2c7f4416..be3e7046d 100644 --- a/pkg/spec/config_unsupported.go +++ b/pkg/spec/config_unsupported.go @@ -30,3 +30,7 @@ func makeThrottleArray(throttleInput []string, rateType int) ([]spec.LinuxThrott func devicesFromPath(g *generate.Generator, devicePath string) error { return errors.New("function not implemented") } + +func deviceCgroupRules(g *generate.Generator, deviceCgroupRules []string) error { + return errors.New("function not implemented") +} diff --git a/pkg/spec/createconfig.go b/pkg/spec/createconfig.go index 173dfb842..8010be0d4 100644 --- a/pkg/spec/createconfig.go +++ b/pkg/spec/createconfig.go @@ -38,6 +38,7 @@ type CreateResourceConfig struct { CPUs float64 // cpus CPUsetCPUs string CPUsetMems string // cpuset-mems + DeviceCgroupRules []string //device-cgroup-rule DeviceReadBps []string // device-read-bps DeviceReadIOps []string // device-read-iops DeviceWriteBps []string // device-write-bps diff --git a/pkg/spec/parse.go b/pkg/spec/parse.go index 6fa0b0636..a5dfccdb9 100644 --- a/pkg/spec/parse.go +++ b/pkg/spec/parse.go @@ -2,12 +2,17 @@ package createconfig import ( "fmt" + "regexp" "strconv" "strings" "github.com/docker/go-units" + "github.com/pkg/errors" ) +// deviceCgroupRulegex defines the valid format of device-cgroup-rule +var deviceCgroupRuleRegex = regexp.MustCompile(`^([acb]) ([0-9]+|\*):([0-9]+|\*) ([rwm]{1,3})$`) + // Pod signifies a kernel namespace is being shared // by a container with the pod it is associated with const Pod = "pod" @@ -205,3 +210,16 @@ func IsValidDeviceMode(mode string) bool { } return true } + +// validateDeviceCgroupRule validates the format of deviceCgroupRule +func validateDeviceCgroupRule(deviceCgroupRule string) error { + if !deviceCgroupRuleRegex.MatchString(deviceCgroupRule) { + return errors.Errorf("invalid device cgroup rule format: '%s'", deviceCgroupRule) + } + return nil +} + +// parseDeviceCgroupRule matches and parses the deviceCgroupRule into slice +func parseDeviceCgroupRule(deviceCgroupRule string) [][]string { + return deviceCgroupRuleRegex.FindAllStringSubmatch(deviceCgroupRule, -1) +} diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index cae055bb0..b2a152a2d 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -232,6 +232,12 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM return nil, err } } + if len(config.Resources.DeviceCgroupRules) != 0 { + if err := deviceCgroupRules(&g, config.Resources.DeviceCgroupRules); err != nil { + return nil, err + } + addedResources = true + } } // SECURITY OPTS diff --git a/test/e2e/config.go b/test/e2e/config.go index 96cc157be..95b0481b3 100644 --- a/test/e2e/config.go +++ b/test/e2e/config.go @@ -2,7 +2,7 @@ package integration var ( redis = "docker.io/library/redis:alpine" - fedoraMinimal = "registry.fedoraproject.org/fedora-minimal:latest" + fedoraMinimal = "quay.io/libpod/fedora-minimal:latest" ALPINE = "docker.io/library/alpine:latest" ALPINELISTTAG = "docker.io/library/alpine:3.10.2" ALPINELISTDIGEST = "docker.io/library/alpine@sha256:72c42ed48c3a2db31b7dafe17d275b634664a708d901ec9fd57b1529280f01fb" diff --git a/test/e2e/images_test.go b/test/e2e/images_test.go index 9a67cc83a..8b6b679a5 100644 --- a/test/e2e/images_test.go +++ b/test/e2e/images_test.go @@ -116,7 +116,8 @@ var _ = Describe("Podman images", func() { }) It("podman images in GO template format", func() { - session := podmanTest.Podman([]string{"images", "--format={{.ID}}"}) + formatStr := "{{.ID}}\t{{.Created}}\t{{.CreatedAt}}\t{{.CreatedSince}}\t{{.CreatedTime}}" + session := podmanTest.Podman([]string{"images", fmt.Sprintf("--format=%s", formatStr)}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) }) @@ -280,7 +281,7 @@ RUN apk update && apk add man return session.OutputToStringArray() } - sortedArr := sortValueTest("created", 0, "CreatedTime") + sortedArr := sortValueTest("created", 0, "CreatedAt") Expect(sort.SliceIsSorted(sortedArr, func(i, j int) bool { return sortedArr[i] > sortedArr[j] })).To(BeTrue()) sortedArr = sortValueTest("id", 0, "ID") diff --git a/test/e2e/load_test.go b/test/e2e/load_test.go index 9ff358d26..9a2cee9e1 100644 --- a/test/e2e/load_test.go +++ b/test/e2e/load_test.go @@ -205,7 +205,7 @@ var _ = Describe("Podman load", func() { podmanTest.RestoreArtifact(fedoraMinimal) outfile := filepath.Join(podmanTest.TempDir, "load_test.tar.gz") - setup := podmanTest.PodmanNoCache([]string{"tag", "fedora-minimal", "hello"}) + setup := podmanTest.PodmanNoCache([]string{"tag", fedoraMinimal, "hello"}) setup.WaitWithDefaultTimeout() Expect(setup.ExitCode()).To(Equal(0)) diff --git a/test/e2e/play_kube_test.go b/test/e2e/play_kube_test.go index 8411e632a..9daf266b8 100644 --- a/test/e2e/play_kube_test.go +++ b/test/e2e/play_kube_test.go @@ -4,6 +4,7 @@ package integration import ( "fmt" + "io/ioutil" "os" "path/filepath" "text/template" @@ -486,4 +487,46 @@ var _ = Describe("Podman generate kube", func() { newBBinspect := inspect.InspectImageJSON() Expect(oldBBinspect[0].Digest).To(Not(Equal(newBBinspect[0].Digest))) }) + + It("podman play kube with image data", func() { + testyaml := ` +apiVersion: v1 +kind: Pod +metadata: + name: demo_pod +spec: + containers: + - image: demo + name: demo_kube +` + pull := podmanTest.Podman([]string{"create", "--workdir", "/etc", "--name", "newBB", "--label", "key1=value1", "alpine"}) + + pull.WaitWithDefaultTimeout() + Expect(pull.ExitCode()).To(BeZero()) + + c := podmanTest.Podman([]string{"commit", "-c", "STOPSIGNAL=51", "newBB", "demo"}) + c.WaitWithDefaultTimeout() + Expect(c.ExitCode()).To(Equal(0)) + + conffile := filepath.Join(podmanTest.TempDir, "kube.yaml") + tempdir, err = CreateTempDirInTempDir() + Expect(err).To(BeNil()) + + err := ioutil.WriteFile(conffile, []byte(testyaml), 0755) + Expect(err).To(BeNil()) + + kube := podmanTest.Podman([]string{"play", "kube", conffile}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).To(Equal(0)) + + inspect := podmanTest.Podman([]string{"inspect", "demo_kube"}) + inspect.WaitWithDefaultTimeout() + Expect(inspect.ExitCode()).To(Equal(0)) + + ctr := inspect.InspectContainerToJSON() + Expect(ctr[0].Config.WorkingDir).To(ContainSubstring("/etc")) + Expect(ctr[0].Config.Labels["key1"]).To(ContainSubstring("value1")) + Expect(ctr[0].Config.Labels["key1"]).To(ContainSubstring("value1")) + Expect(ctr[0].Config.StopSignal).To(Equal(uint(51))) + }) }) diff --git a/test/e2e/ps_test.go b/test/e2e/ps_test.go index fccc5c93b..48dd186e2 100644 --- a/test/e2e/ps_test.go +++ b/test/e2e/ps_test.go @@ -243,6 +243,19 @@ var _ = Describe("Podman ps", func() { Expect(psAll.OutputToString()).To(Equal(psFilter.OutputToString())) }) + It("podman filter without status does not find non-running", func() { + ctrName := "aContainerName" + ctr := podmanTest.Podman([]string{"create", "--name", ctrName, "-t", "-i", ALPINE, "ls", "/"}) + ctr.WaitWithDefaultTimeout() + Expect(ctr.ExitCode()).To(Equal(0)) + + psFilter := podmanTest.Podman([]string{"ps", "--no-trunc", "--quiet", "--format", "{{.Names}}", "--filter", fmt.Sprintf("name=%s", ctrName)}) + psFilter.WaitWithDefaultTimeout() + Expect(psFilter.ExitCode()).To(Equal(0)) + + Expect(strings.Contains(psFilter.OutputToString(), ctrName)).To(BeFalse()) + }) + It("podman ps mutually exclusive flags", func() { session := podmanTest.Podman([]string{"ps", "-aqs"}) session.WaitWithDefaultTimeout() diff --git a/test/e2e/run_test.go b/test/e2e/run_test.go index 72547ea00..3eb93b84a 100644 --- a/test/e2e/run_test.go +++ b/test/e2e/run_test.go @@ -999,4 +999,16 @@ USER mail` session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Not(Equal(0))) }) + + It("podman run --device-cgroup-rule", func() { + SkipIfRemote() + SkipIfRootless() + deviceCgroupRule := "c 42:* rwm" + session := podmanTest.Podman([]string{"run", "--name", "test", "-d", "--device-cgroup-rule", deviceCgroupRule, ALPINE, "top"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + session = podmanTest.Podman([]string{"exec", "test", "mknod", "newDev", "c", "42", "1"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + }) }) diff --git a/test/endpoint/endpoint.go b/test/endpoint/endpoint.go index 78aa957ab..f1634b6f0 100644 --- a/test/endpoint/endpoint.go +++ b/test/endpoint/endpoint.go @@ -29,7 +29,7 @@ var ( infra = "k8s.gcr.io/pause:3.1" BB = "docker.io/library/busybox:latest" redis = "docker.io/library/redis:alpine" - fedoraMinimal = "registry.fedoraproject.org/fedora-minimal:latest" + fedoraMinimal = "quay.io/libpod/fedora-minimal:latest" ) type EndpointTestIntegration struct { diff --git a/vendor/github.com/gorilla/mux/README.md b/vendor/github.com/gorilla/mux/README.md index 92e422eed..35eea9f10 100644 --- a/vendor/github.com/gorilla/mux/README.md +++ b/vendor/github.com/gorilla/mux/README.md @@ -1,11 +1,10 @@ # gorilla/mux [![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) -[![Build Status](https://travis-ci.org/gorilla/mux.svg?branch=master)](https://travis-ci.org/gorilla/mux) [![CircleCI](https://circleci.com/gh/gorilla/mux.svg?style=svg)](https://circleci.com/gh/gorilla/mux) [![Sourcegraph](https://sourcegraph.com/github.com/gorilla/mux/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/mux?badge) -![Gorilla Logo](http://www.gorillatoolkit.org/static/images/gorilla-icon-64.png) +![Gorilla Logo](https://cloud-cdn.questionable.services/gorilla-icon-64.png) https://www.gorillatoolkit.org/pkg/mux @@ -26,6 +25,7 @@ The name mux stands for "HTTP request multiplexer". Like the standard `http.Serv * [Examples](#examples) * [Matching Routes](#matching-routes) * [Static Files](#static-files) +* [Serving Single Page Applications](#serving-single-page-applications) (e.g. React, Vue, Ember.js, etc.) * [Registered URLs](#registered-urls) * [Walking Routes](#walking-routes) * [Graceful Shutdown](#graceful-shutdown) @@ -212,6 +212,93 @@ func main() { } ``` +### Serving Single Page Applications + +Most of the time it makes sense to serve your SPA on a separate web server from your API, +but sometimes it's desirable to serve them both from one place. It's possible to write a simple +handler for serving your SPA (for use with React Router's [BrowserRouter](https://reacttraining.com/react-router/web/api/BrowserRouter) for example), and leverage +mux's powerful routing for your API endpoints. + +```go +package main + +import ( + "encoding/json" + "log" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/gorilla/mux" +) + +// spaHandler implements the http.Handler interface, so we can use it +// to respond to HTTP requests. The path to the static directory and +// path to the index file within that static directory are used to +// serve the SPA in the given static directory. +type spaHandler struct { + staticPath string + indexPath string +} + +// ServeHTTP inspects the URL path to locate a file within the static dir +// on the SPA handler. If a file is found, it will be served. If not, the +// file located at the index path on the SPA handler will be served. This +// is suitable behavior for serving an SPA (single page application). +func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // get the absolute path to prevent directory traversal + path, err := filepath.Abs(r.URL.Path) + if err != nil { + // if we failed to get the absolute path respond with a 400 bad request + // and stop + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // prepend the path with the path to the static directory + path = filepath.Join(h.staticPath, path) + + // check whether a file exists at the given path + _, err = os.Stat(path) + if os.IsNotExist(err) { + // file does not exist, serve index.html + http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath)) + return + } else if err != nil { + // if we got an error (that wasn't that the file doesn't exist) stating the + // file, return a 500 internal server error and stop + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // otherwise, use http.FileServer to serve the static dir + http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r) +} + +func main() { + router := mux.NewRouter() + + router.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { + // an example API handler + json.NewEncoder(w).Encode(map[string]bool{"ok": true}) + }) + + spa := spaHandler{staticPath: "build", indexPath: "index.html"} + router.PathPrefix("/").Handler(spa) + + srv := &http.Server{ + Handler: router, + Addr: "127.0.0.1:8000", + // Good practice: enforce timeouts for servers you create! + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + log.Fatal(srv.ListenAndServe()) +} +``` + ### Registered URLs Now let's see how to build registered URLs. diff --git a/vendor/github.com/gorilla/mux/context.go b/vendor/github.com/gorilla/mux/context.go deleted file mode 100644 index 665940a26..000000000 --- a/vendor/github.com/gorilla/mux/context.go +++ /dev/null @@ -1,18 +0,0 @@ -package mux - -import ( - "context" - "net/http" -) - -func contextGet(r *http.Request, key interface{}) interface{} { - return r.Context().Value(key) -} - -func contextSet(r *http.Request, key, val interface{}) *http.Request { - if val == nil { - return r - } - - return r.WithContext(context.WithValue(r.Context(), key, val)) -} diff --git a/vendor/github.com/gorilla/mux/go.mod b/vendor/github.com/gorilla/mux/go.mod index cfc8ede58..df170a399 100644 --- a/vendor/github.com/gorilla/mux/go.mod +++ b/vendor/github.com/gorilla/mux/go.mod @@ -1 +1,3 @@ module github.com/gorilla/mux + +go 1.12 diff --git a/vendor/github.com/gorilla/mux/middleware.go b/vendor/github.com/gorilla/mux/middleware.go index cf2b26dc0..cb51c565e 100644 --- a/vendor/github.com/gorilla/mux/middleware.go +++ b/vendor/github.com/gorilla/mux/middleware.go @@ -58,22 +58,17 @@ func CORSMethodMiddleware(r *Router) MiddlewareFunc { func getAllMethodsForRoute(r *Router, req *http.Request) ([]string, error) { var allMethods []string - err := r.Walk(func(route *Route, _ *Router, _ []*Route) error { - for _, m := range route.matchers { - if _, ok := m.(*routeRegexp); ok { - if m.Match(req, &RouteMatch{}) { - methods, err := route.GetMethods() - if err != nil { - return err - } - - allMethods = append(allMethods, methods...) - } - break + for _, route := range r.routes { + var match RouteMatch + if route.Match(req, &match) || match.MatchErr == ErrMethodMismatch { + methods, err := route.GetMethods() + if err != nil { + return nil, err } + + allMethods = append(allMethods, methods...) } - return nil - }) + } - return allMethods, err + return allMethods, nil } diff --git a/vendor/github.com/gorilla/mux/mux.go b/vendor/github.com/gorilla/mux/mux.go index a2cd193e4..c9ba64707 100644 --- a/vendor/github.com/gorilla/mux/mux.go +++ b/vendor/github.com/gorilla/mux/mux.go @@ -5,6 +5,7 @@ package mux import ( + "context" "errors" "fmt" "net/http" @@ -58,8 +59,7 @@ type Router struct { // If true, do not clear the request context after handling the request. // - // Deprecated: No effect when go1.7+ is used, since the context is stored - // on the request itself. + // Deprecated: No effect, since the context is stored on the request itself. KeepContext bool // Slice of middlewares to be called after a match is found @@ -111,10 +111,8 @@ func copyRouteConf(r routeConf) routeConf { c.regexp.queries = append(c.regexp.queries, copyRouteRegexp(q)) } - c.matchers = make([]matcher, 0, len(r.matchers)) - for _, m := range r.matchers { - c.matchers = append(c.matchers, m) - } + c.matchers = make([]matcher, len(r.matchers)) + copy(c.matchers, r.matchers) return c } @@ -197,8 +195,8 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { var handler http.Handler if r.Match(req, &match) { handler = match.Handler - req = setVars(req, match.Vars) - req = setCurrentRoute(req, match.Route) + req = requestWithVars(req, match.Vars) + req = requestWithRoute(req, match.Route) } if handler == nil && match.MatchErr == ErrMethodMismatch { @@ -428,7 +426,7 @@ const ( // Vars returns the route variables for the current request, if any. func Vars(r *http.Request) map[string]string { - if rv := contextGet(r, varsKey); rv != nil { + if rv := r.Context().Value(varsKey); rv != nil { return rv.(map[string]string) } return nil @@ -440,18 +438,20 @@ func Vars(r *http.Request) map[string]string { // after the handler returns, unless the KeepContext option is set on the // Router. func CurrentRoute(r *http.Request) *Route { - if rv := contextGet(r, routeKey); rv != nil { + if rv := r.Context().Value(routeKey); rv != nil { return rv.(*Route) } return nil } -func setVars(r *http.Request, val interface{}) *http.Request { - return contextSet(r, varsKey, val) +func requestWithVars(r *http.Request, vars map[string]string) *http.Request { + ctx := context.WithValue(r.Context(), varsKey, vars) + return r.WithContext(ctx) } -func setCurrentRoute(r *http.Request, val interface{}) *http.Request { - return contextSet(r, routeKey, val) +func requestWithRoute(r *http.Request, route *Route) *http.Request { + ctx := context.WithValue(r.Context(), routeKey, route) + return r.WithContext(ctx) } // ---------------------------------------------------------------------------- diff --git a/vendor/github.com/gorilla/mux/regexp.go b/vendor/github.com/gorilla/mux/regexp.go index ac1abcd47..96dd94ad1 100644 --- a/vendor/github.com/gorilla/mux/regexp.go +++ b/vendor/github.com/gorilla/mux/regexp.go @@ -181,21 +181,21 @@ func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { } } return r.regexp.MatchString(host) - } else { - if r.regexpType == regexpTypeQuery { - return r.matchQueryString(req) - } - path := req.URL.Path - if r.options.useEncodedPath { - path = req.URL.EscapedPath() - } - return r.regexp.MatchString(path) } + + if r.regexpType == regexpTypeQuery { + return r.matchQueryString(req) + } + path := req.URL.Path + if r.options.useEncodedPath { + path = req.URL.EscapedPath() + } + return r.regexp.MatchString(path) } // url builds a URL part using the given values. func (r *routeRegexp) url(values map[string]string) (string, error) { - urlValues := make([]interface{}, len(r.varsN)) + urlValues := make([]interface{}, len(r.varsN), len(r.varsN)) for k, v := range r.varsN { value, ok := values[v] if !ok { @@ -230,14 +230,51 @@ func (r *routeRegexp) getURLQuery(req *http.Request) string { return "" } templateKey := strings.SplitN(r.template, "=", 2)[0] - for key, vals := range req.URL.Query() { - if key == templateKey && len(vals) > 0 { - return key + "=" + vals[0] - } + val, ok := findFirstQueryKey(req.URL.RawQuery, templateKey) + if ok { + return templateKey + "=" + val } return "" } +// findFirstQueryKey returns the same result as (*url.URL).Query()[key][0]. +// If key was not found, empty string and false is returned. +func findFirstQueryKey(rawQuery, key string) (value string, ok bool) { + query := []byte(rawQuery) + for len(query) > 0 { + foundKey := query + if i := bytes.IndexAny(foundKey, "&;"); i >= 0 { + foundKey, query = foundKey[:i], foundKey[i+1:] + } else { + query = query[:0] + } + if len(foundKey) == 0 { + continue + } + var value []byte + if i := bytes.IndexByte(foundKey, '='); i >= 0 { + foundKey, value = foundKey[:i], foundKey[i+1:] + } + if len(foundKey) < len(key) { + // Cannot possibly be key. + continue + } + keyString, err := url.QueryUnescape(string(foundKey)) + if err != nil { + continue + } + if keyString != key { + continue + } + valueString, err := url.QueryUnescape(string(value)) + if err != nil { + continue + } + return valueString, true + } + return "", false +} + func (r *routeRegexp) matchQueryString(req *http.Request) bool { return r.regexp.MatchString(r.getURLQuery(req)) } diff --git a/vendor/github.com/gorilla/mux/route.go b/vendor/github.com/gorilla/mux/route.go index 8479c68c1..750afe570 100644 --- a/vendor/github.com/gorilla/mux/route.go +++ b/vendor/github.com/gorilla/mux/route.go @@ -74,7 +74,7 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool { return false } - if match.MatchErr == ErrMethodMismatch { + if match.MatchErr == ErrMethodMismatch && r.handler != nil { // We found a route which matches request method, clear MatchErr match.MatchErr = nil // Then override the mis-matched handler @@ -412,11 +412,30 @@ func (r *Route) Queries(pairs ...string) *Route { type schemeMatcher []string func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool { - return matchInArray(m, r.URL.Scheme) + scheme := r.URL.Scheme + // https://golang.org/pkg/net/http/#Request + // "For [most] server requests, fields other than Path and RawQuery will be + // empty." + // Since we're an http muxer, the scheme is either going to be http or https + // though, so we can just set it based on the tls termination state. + if scheme == "" { + if r.TLS == nil { + scheme = "http" + } else { + scheme = "https" + } + } + return matchInArray(m, scheme) } // Schemes adds a matcher for URL schemes. // It accepts a sequence of schemes to be matched, e.g.: "http", "https". +// If the request's URL has a scheme set, it will be matched against. +// Generally, the URL scheme will only be set if a previous handler set it, +// such as the ProxyHeaders handler from gorilla/handlers. +// If unset, the scheme will be determined based on the request's TLS +// termination state. +// The first argument to Schemes will be used when constructing a route URL. func (r *Route) Schemes(schemes ...string) *Route { for k, v := range schemes { schemes[k] = strings.ToLower(v) @@ -493,8 +512,8 @@ func (r *Route) Subrouter() *Router { // This also works for host variables: // // r := mux.NewRouter() -// r.Host("{subdomain}.domain.com"). -// HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). +// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). +// Host("{subdomain}.domain.com"). // Name("article") // // // url.String() will be "http://news.domain.com/articles/technology/42" @@ -502,6 +521,13 @@ func (r *Route) Subrouter() *Router { // "category", "technology", // "id", "42") // +// The scheme of the resulting url will be the first argument that was passed to Schemes: +// +// // url.String() will be "https://example.com" +// r := mux.NewRouter() +// url, err := r.Host("example.com") +// .Schemes("https", "http").URL() +// // All variables defined in the route are required, and their values must // conform to the corresponding patterns. func (r *Route) URL(pairs ...string) (*url.URL, error) { @@ -635,7 +661,7 @@ func (r *Route) GetQueriesRegexp() ([]string, error) { if r.regexp.queries == nil { return nil, errors.New("mux: route doesn't have queries") } - var queries []string + queries := make([]string, 0, len(r.regexp.queries)) for _, query := range r.regexp.queries { queries = append(queries, query.regexp.String()) } @@ -654,7 +680,7 @@ func (r *Route) GetQueriesTemplates() ([]string, error) { if r.regexp.queries == nil { return nil, errors.New("mux: route doesn't have queries") } - var queries []string + queries := make([]string, 0, len(r.regexp.queries)) for _, query := range r.regexp.queries { queries = append(queries, query.template) } diff --git a/vendor/github.com/gorilla/mux/test_helpers.go b/vendor/github.com/gorilla/mux/test_helpers.go index 32ecffde4..5f5c496de 100644 --- a/vendor/github.com/gorilla/mux/test_helpers.go +++ b/vendor/github.com/gorilla/mux/test_helpers.go @@ -15,5 +15,5 @@ import "net/http" // can be set by making a route that captures the required variables, // starting a server and sending the request to that server. func SetURLVars(r *http.Request, val map[string]string) *http.Request { - return setVars(r, val) + return requestWithVars(r, val) } diff --git a/vendor/modules.txt b/vendor/modules.txt index 5c2485f38..73bca1ef8 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -291,7 +291,7 @@ github.com/google/gofuzz github.com/google/shlex # github.com/google/uuid v1.1.1 github.com/google/uuid -# github.com/gorilla/mux v1.7.3 +# github.com/gorilla/mux v1.7.4 github.com/gorilla/mux # github.com/gorilla/schema v1.1.0 github.com/gorilla/schema @@ -630,9 +630,9 @@ gopkg.in/square/go-jose.v2/json gopkg.in/tomb.v1 # gopkg.in/yaml.v2 v2.2.8 gopkg.in/yaml.v2 -# k8s.io/api v0.17.2 +# k8s.io/api v0.17.3 k8s.io/api/core/v1 -# k8s.io/apimachinery v0.17.2 +# k8s.io/apimachinery v0.17.3 k8s.io/apimachinery/pkg/api/errors k8s.io/apimachinery/pkg/api/resource k8s.io/apimachinery/pkg/apis/meta/v1 |