diff options
Diffstat (limited to 'pkg')
42 files changed, 722 insertions, 279 deletions
diff --git a/pkg/api/handlers/compat/containers_attach.go b/pkg/api/handlers/compat/containers_attach.go index e20d48d86..4a1196c89 100644 --- a/pkg/api/handlers/compat/containers_attach.go +++ b/pkg/api/handlers/compat/containers_attach.go @@ -6,7 +6,7 @@ import ( "github.com/containers/podman/v2/libpod" "github.com/containers/podman/v2/libpod/define" "github.com/containers/podman/v2/pkg/api/handlers/utils" - "github.com/containers/podman/v2/pkg/api/server/idletracker" + "github.com/containers/podman/v2/pkg/api/server/idle" "github.com/gorilla/schema" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -92,7 +92,7 @@ func AttachContainer(w http.ResponseWriter, r *http.Request) { return } - idleTracker := r.Context().Value("idletracker").(*idletracker.IdleTracker) + idleTracker := r.Context().Value("idletracker").(*idle.Tracker) hijackChan := make(chan bool, 1) // Perform HTTP attach. @@ -109,7 +109,7 @@ func AttachContainer(w http.ResponseWriter, r *http.Request) { // We do need to tell the idle tracker that the // connection has been closed, though. We can guarantee // that is true after HTTPAttach exits. - idleTracker.TrackHijackedClosed() + idleTracker.Close() } else { // A hijack was not successfully completed. We need to // report the error normally. diff --git a/pkg/api/handlers/compat/containers_create.go b/pkg/api/handlers/compat/containers_create.go index 1d0b4c45d..0579da8de 100644 --- a/pkg/api/handlers/compat/containers_create.go +++ b/pkg/api/handlers/compat/containers_create.go @@ -82,7 +82,13 @@ func makeCreateConfig(ctx context.Context, containerConfig *config.Config, input } } - workDir := "/" + workDir, err := newImage.WorkingDir(ctx) + if err != nil { + return createconfig.CreateConfig{}, err + } + if workDir == "" { + workDir = "/" + } if len(input.WorkingDir) > 0 { workDir = input.WorkingDir } @@ -169,6 +175,11 @@ func makeCreateConfig(ctx context.Context, containerConfig *config.Config, input // away incorrectly formatted variables so we cannot reuse the // parsing of the env input // [Foo Other=one Blank=] + imgEnv, err := newImage.Env(ctx) + if err != nil { + return createconfig.CreateConfig{}, err + } + input.Env = append(imgEnv, input.Env...) for _, e := range input.Env { splitEnv := strings.Split(e, "=") switch len(splitEnv) { diff --git a/pkg/api/handlers/compat/exec.go b/pkg/api/handlers/compat/exec.go index 1db950f85..df51293c2 100644 --- a/pkg/api/handlers/compat/exec.go +++ b/pkg/api/handlers/compat/exec.go @@ -10,7 +10,7 @@ import ( "github.com/containers/podman/v2/libpod/define" "github.com/containers/podman/v2/pkg/api/handlers" "github.com/containers/podman/v2/pkg/api/handlers/utils" - "github.com/containers/podman/v2/pkg/api/server/idletracker" + "github.com/containers/podman/v2/pkg/api/server/idle" "github.com/containers/podman/v2/pkg/specgen/generate" "github.com/gorilla/mux" "github.com/pkg/errors" @@ -174,7 +174,7 @@ func ExecStartHandler(w http.ResponseWriter, r *http.Request) { return } - idleTracker := r.Context().Value("idletracker").(*idletracker.IdleTracker) + idleTracker := r.Context().Value("idletracker").(*idle.Tracker) hijackChan := make(chan bool, 1) if err := sessionCtr.ExecHTTPStartAndAttach(sessionID, r, w, nil, nil, nil, hijackChan); err != nil { @@ -186,7 +186,7 @@ func ExecStartHandler(w http.ResponseWriter, r *http.Request) { // We do need to tell the idle tracker that the // connection has been closed, though. We can guarantee // that is true after HTTPAttach exits. - idleTracker.TrackHijackedClosed() + idleTracker.Close() } else { // A hijack was not successfully completed. We need to // report the error normally. diff --git a/pkg/api/handlers/compat/images.go b/pkg/api/handlers/compat/images.go index 0900d1793..940b57343 100644 --- a/pkg/api/handlers/compat/images.go +++ b/pkg/api/handlers/compat/images.go @@ -93,7 +93,7 @@ func PruneImages(w http.ResponseWriter, r *http.Request) { }) } - //FIXME/TODO to do this exactly correct, pruneimages needs to return idrs and space-reclaimed, then we are golden + // FIXME/TODO to do this exactly correct, pruneimages needs to return idrs and space-reclaimed, then we are golden ipr := types.ImagesPruneReport{ ImagesDeleted: idr, SpaceReclaimed: 1, // TODO we cannot supply this right now @@ -113,7 +113,7 @@ func CommitContainer(w http.ResponseWriter, r *http.Request) { Changes string `schema:"changes"` Comment string `schema:"comment"` Container string `schema:"container"` - //fromSrc string # fromSrc is currently unused + // fromSrc string # fromSrc is currently unused Pause bool `schema:"pause"` Repo string `schema:"repo"` Tag string `schema:"tag"` @@ -224,7 +224,7 @@ func CreateImageFromSrc(w http.ResponseWriter, r *http.Request) { Status string `json:"status"` Progress string `json:"progress"` ProgressDetail map[string]string `json:"progressDetail"` - Id string `json:"id"` //nolint + Id string `json:"id"` // nolint }{ Status: iid, ProgressDetail: map[string]string{}, @@ -257,9 +257,9 @@ func CreateImageFromImage(w http.ResponseWriter, r *http.Request) { fromImage = fmt.Sprintf("%s:%s", fromImage, query.Tag) } - authConf, authfile, err := auth.GetCredentials(r) + authConf, authfile, key, err := auth.GetCredentials(r) if err != nil { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse %q header for %s", auth.XRegistryAuthHeader, r.URL.String())) + utils.Error(w, "Failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse %q header for %s", key, r.URL.String())) return } defer auth.RemoveAuthfile(authfile) @@ -299,7 +299,7 @@ func CreateImageFromImage(w http.ResponseWriter, r *http.Request) { Error string `json:"error"` Progress string `json:"progress"` ProgressDetail map[string]string `json:"progressDetail"` - Id string `json:"id"` //nolint + Id string `json:"id"` // nolint }{ Status: fmt.Sprintf("pulling image (%s) from %s", img.Tag, strings.Join(img.Names(), ", ")), ProgressDetail: map[string]string{}, diff --git a/pkg/api/handlers/compat/images_build.go b/pkg/api/handlers/compat/images_build.go index fbaf8d10a..d5ccf56fe 100644 --- a/pkg/api/handlers/compat/images_build.go +++ b/pkg/api/handlers/compat/images_build.go @@ -2,7 +2,6 @@ package compat import ( "context" - "encoding/base64" "encoding/json" "fmt" "io" @@ -11,13 +10,13 @@ import ( "os" "path/filepath" "strconv" - "strings" "github.com/containers/buildah" "github.com/containers/buildah/imagebuildah" + "github.com/containers/image/v5/types" "github.com/containers/podman/v2/libpod" - "github.com/containers/podman/v2/pkg/api/handlers" "github.com/containers/podman/v2/pkg/api/handlers/utils" + "github.com/containers/podman/v2/pkg/auth" "github.com/containers/podman/v2/pkg/channel" "github.com/containers/storage/pkg/archive" "github.com/gorilla/schema" @@ -26,15 +25,6 @@ import ( ) func BuildImage(w http.ResponseWriter, r *http.Request) { - authConfigs := map[string]handlers.AuthConfig{} - if hdr, found := r.Header["X-Registry-Config"]; found && len(hdr) > 0 { - authConfigsJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(hdr[0])) - if json.NewDecoder(authConfigsJSON).Decode(&authConfigs) != nil { - utils.BadRequest(w, "X-Registry-Config", hdr[0], json.NewDecoder(authConfigsJSON).Decode(&authConfigs)) - return - } - } - if hdr, found := r.Header["Content-Type"]; found && len(hdr) > 0 { contentType := hdr[0] switch contentType { @@ -70,37 +60,38 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { }() query := struct { + BuildArgs string `schema:"buildargs"` + CacheFrom string `schema:"cachefrom"` + CpuPeriod uint64 `schema:"cpuperiod"` // nolint + CpuQuota int64 `schema:"cpuquota"` // nolint + CpuSetCpus string `schema:"cpusetcpus"` // nolint + CpuShares uint64 `schema:"cpushares"` // nolint Dockerfile string `schema:"dockerfile"` - Tag []string `schema:"t"` ExtraHosts string `schema:"extrahosts"` - Remote string `schema:"remote"` - Quiet bool `schema:"q"` + ForceRm bool `schema:"forcerm"` + HTTPProxy bool `schema:"httpproxy"` + Labels string `schema:"labels"` + MemSwap int64 `schema:"memswap"` + Memory int64 `schema:"memory"` + NetworkMode string `schema:"networkmode"` NoCache bool `schema:"nocache"` - CacheFrom string `schema:"cachefrom"` + Outputs string `schema:"outputs"` + Platform string `schema:"platform"` Pull bool `schema:"pull"` + Quiet bool `schema:"q"` + Registry string `schema:"registry"` + Remote string `schema:"remote"` Rm bool `schema:"rm"` - ForceRm bool `schema:"forcerm"` - Memory int64 `schema:"memory"` - MemSwap int64 `schema:"memswap"` - CpuShares uint64 `schema:"cpushares"` // nolint - CpuSetCpus string `schema:"cpusetcpus"` // nolint - CpuPeriod uint64 `schema:"cpuperiod"` // nolint - CpuQuota int64 `schema:"cpuquota"` // nolint - BuildArgs string `schema:"buildargs"` ShmSize int `schema:"shmsize"` Squash bool `schema:"squash"` - Labels string `schema:"labels"` - NetworkMode string `schema:"networkmode"` - Platform string `schema:"platform"` + Tag []string `schema:"t"` Target string `schema:"target"` - Outputs string `schema:"outputs"` - Registry string `schema:"registry"` }{ Dockerfile: "Dockerfile", - Tag: []string{}, + Registry: "docker.io", Rm: true, ShmSize: 64 * 1024 * 1024, - Registry: "docker.io", + Tag: []string{}, } decoder := r.Context().Value("decoder").(*schema.Decoder) @@ -151,6 +142,14 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { } } + creds, authfile, key, err := auth.GetCredentials(r) + if err != nil { + // Credential value(s) not returned as their value is not human readable + utils.BadRequest(w, key.String(), "n/a", err) + return + } + defer auth.RemoveAuthfile(authfile) + // Channels all mux'ed in select{} below to follow API build protocol stdout := channel.NewWriter(make(chan []byte, 1)) defer stdout.Close() @@ -179,11 +178,16 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { Err: auxout, ReportWriter: reporter, OutputFormat: buildah.Dockerv2ImageManifest, + SystemContext: &types.SystemContext{ + AuthFilePath: authfile, + DockerAuthConfig: creds, + }, CommonBuildOpts: &buildah.CommonBuildOptions{ CPUPeriod: query.CpuPeriod, CPUQuota: query.CpuQuota, CPUShares: query.CpuShares, CPUSetCPUs: query.CpuSetCpus, + HTTPProxy: query.HTTPProxy, Memory: query.Memory, MemorySwap: query.MemSwap, ShmSize: strconv.Itoa(query.ShmSize), diff --git a/pkg/api/handlers/compat/images_push.go b/pkg/api/handlers/compat/images_push.go index e69a2212a..dd706a156 100644 --- a/pkg/api/handlers/compat/images_push.go +++ b/pkg/api/handlers/compat/images_push.go @@ -49,9 +49,9 @@ func PushImage(w http.ResponseWriter, r *http.Request) { return } - authConf, authfile, err := auth.GetCredentials(r) + authConf, authfile, key, err := auth.GetCredentials(r) if err != nil { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse %q header for %s", auth.XRegistryAuthHeader, r.URL.String())) + utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse %q header for %s", key, r.URL.String())) return } defer auth.RemoveAuthfile(authfile) diff --git a/pkg/api/handlers/compat/volumes.go b/pkg/api/handlers/compat/volumes.go index 976c52acb..a45509fdb 100644 --- a/pkg/api/handlers/compat/volumes.go +++ b/pkg/api/handlers/compat/volumes.go @@ -93,6 +93,29 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) { return } + // See if the volume exists already + existingVolume, err := runtime.GetVolume(input.Name) + if err != nil && errors.Cause(err) != define.ErrNoSuchVolume { + utils.InternalServerError(w, err) + return + } + + // if using the compat layer and the volume already exists, we + // must return a 201 with the same information as create + if existingVolume != nil && !utils.IsLibpodRequest(r) { + response := docker_api_types.Volume{ + CreatedAt: existingVolume.CreatedTime().Format(time.RFC3339), + Driver: existingVolume.Driver(), + Labels: existingVolume.Labels(), + Mountpoint: existingVolume.MountPoint(), + Name: existingVolume.Name(), + Options: existingVolume.Options(), + Scope: existingVolume.Scope(), + } + utils.WriteResponse(w, http.StatusCreated, response) + return + } + if len(input.Name) > 0 { volumeOptions = append(volumeOptions, libpod.WithVolumeName(input.Name)) } diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index bc1bdc287..3054922c2 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -438,9 +438,9 @@ func PushImage(w http.ResponseWriter, r *http.Request) { return } - authConf, authfile, err := auth.GetCredentials(r) + authConf, authfile, key, err := auth.GetCredentials(r) if err != nil { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse %q header for %s", auth.XRegistryAuthHeader, r.URL.String())) + utils.Error(w, "Failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse %q header for %s", key, r.URL.String())) return } defer auth.RemoveAuthfile(authfile) @@ -560,24 +560,41 @@ func CommitContainer(w http.ResponseWriter, r *http.Request) { func UntagImage(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) - name := utils.GetName(r) - newImage, err := runtime.ImageRuntime().NewFromLocal(name) - if err != nil { - utils.ImageNotFound(w, name, errors.Wrapf(err, "Failed to find image %s", name)) - return - } - tag := "latest" - if len(r.Form.Get("tag")) > 0 { - tag = r.Form.Get("tag") - } - if len(r.Form.Get("repo")) < 1 { + tags := []string{} // Note: if empty, all tags will be removed from the image. + repo := r.Form.Get("repo") + tag := r.Form.Get("tag") + + // Do the parameter dance. + switch { + // If tag is set, repo must be as well. + case len(repo) == 0 && len(tag) > 0: utils.Error(w, "repo tag is required", http.StatusBadRequest, errors.New("repo parameter is required to tag an image")) return + + case len(repo) == 0: + break + + // If repo is specified, we need to add that to the tags. + default: + if len(tag) == 0 { + // Normalize tag to "latest" if empty. + tag = "latest" + } + tags = append(tags, fmt.Sprintf("%s:%s", repo, tag)) } - repo := r.Form.Get("repo") - tagName := fmt.Sprintf("%s:%s", repo, tag) - if err := newImage.UntagImage(tagName); err != nil { - utils.Error(w, "failed to untag", http.StatusInternalServerError, err) + + // Now use the ABI implementation to prevent us from having duplicate + // code. + opts := entities.ImageUntagOptions{} + imageEngine := abi.ImageEngine{Libpod: runtime} + + name := utils.GetName(r) + if err := imageEngine.Untag(r.Context(), name, tags, opts); err != nil { + if errors.Cause(err) == define.ErrNoSuchImage { + utils.ImageNotFound(w, name, errors.Wrapf(err, "Failed to find image %s", name)) + } else { + utils.Error(w, "failed to untag", http.StatusInternalServerError, err) + } return } utils.WriteResponse(w, http.StatusCreated, "") diff --git a/pkg/api/handlers/libpod/images_pull.go b/pkg/api/handlers/libpod/images_pull.go index ad8d1f38e..791ef7a48 100644 --- a/pkg/api/handlers/libpod/images_pull.go +++ b/pkg/api/handlers/libpod/images_pull.go @@ -74,9 +74,9 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) { return } - authConf, authfile, err := auth.GetCredentials(r) + authConf, authfile, key, err := auth.GetCredentials(r) if err != nil { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse %q header for %s", auth.XRegistryAuthHeader, r.URL.String())) + utils.Error(w, "Failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse %q header for %s", key, r.URL.String())) return } defer auth.RemoveAuthfile(authfile) diff --git a/pkg/api/handlers/libpod/manifests.go b/pkg/api/handlers/libpod/manifests.go index 8e65248e2..2031dd42f 100644 --- a/pkg/api/handlers/libpod/manifests.go +++ b/pkg/api/handlers/libpod/manifests.go @@ -6,11 +6,13 @@ import ( "github.com/containers/buildah/manifests" copy2 "github.com/containers/image/v5/copy" + "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/transports/alltransports" "github.com/containers/podman/v2/libpod" "github.com/containers/podman/v2/libpod/image" "github.com/containers/podman/v2/pkg/api/handlers" "github.com/containers/podman/v2/pkg/api/handlers/utils" + "github.com/containers/podman/v2/pkg/domain/infra/abi" "github.com/gorilla/schema" "github.com/opencontainers/go-digest" "github.com/pkg/errors" @@ -48,17 +50,18 @@ func ManifestCreate(w http.ResponseWriter, r *http.Request) { func ManifestInspect(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) name := utils.GetName(r) - newImage, err := runtime.ImageRuntime().NewFromLocal(name) - if err != nil { - utils.ImageNotFound(w, name, err) + imageEngine := abi.ImageEngine{Libpod: runtime} + inspectReport, inspectError := imageEngine.ManifestInspect(r.Context(), name) + if inspectError != nil { + utils.Error(w, "Something went wrong.", http.StatusNotFound, inspectError) return } - data, err := newImage.InspectManifest() - if err != nil { - utils.InternalServerError(w, err) + var list manifest.Schema2List + if err := json.Unmarshal(inspectReport, &list); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Unmarshal()")) return } - utils.WriteResponse(w, http.StatusOK, data) + utils.WriteResponse(w, http.StatusOK, &list) } func ManifestAdd(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/api/handlers/libpod/networks.go b/pkg/api/handlers/libpod/networks.go index dfece2a4e..b3c4840b8 100644 --- a/pkg/api/handlers/libpod/networks.go +++ b/pkg/api/handlers/libpod/networks.go @@ -92,8 +92,8 @@ func RemoveNetwork(w http.ResponseWriter, r *http.Request) { } if reports[0].Err != nil { // If the network cannot be found, we return a 404. - if errors.Cause(err) == define.ErrNoSuchNetwork { - utils.Error(w, "Something went wrong", http.StatusNotFound, err) + if errors.Cause(reports[0].Err) == define.ErrNoSuchNetwork { + utils.Error(w, "Something went wrong", http.StatusNotFound, reports[0].Err) return } } diff --git a/pkg/api/handlers/libpod/play.go b/pkg/api/handlers/libpod/play.go index 59f78da8c..2296e170a 100644 --- a/pkg/api/handlers/libpod/play.go +++ b/pkg/api/handlers/libpod/play.go @@ -48,9 +48,9 @@ func PlayKube(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error closing temporary file")) return } - authConf, authfile, err := auth.GetCredentials(r) + authConf, authfile, key, err := auth.GetCredentials(r) if err != nil { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse %q header for %s", auth.XRegistryAuthHeader, r.URL.String())) + utils.Error(w, "Failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse %q header for %s", key, r.URL.String())) return } defer auth.RemoveAuthfile(authfile) diff --git a/pkg/api/server/handler_api.go b/pkg/api/server/handler_api.go index 920811c51..28f5a0b42 100644 --- a/pkg/api/server/handler_api.go +++ b/pkg/api/server/handler_api.go @@ -7,7 +7,9 @@ import ( "runtime" "github.com/containers/podman/v2/pkg/api/handlers/utils" - log "github.com/sirupsen/logrus" + "github.com/containers/podman/v2/pkg/auth" + "github.com/google/uuid" + "github.com/sirupsen/logrus" ) // APIHandler is a wrapper to enhance HandlerFunc's and remove redundant code @@ -19,7 +21,7 @@ func (s *APIServer) APIHandler(h http.HandlerFunc) http.HandlerFunc { if err != nil { buf := make([]byte, 1<<20) n := runtime.Stack(buf, true) - log.Warnf("Recovering from API handler panic: %v, %s", err, buf[:n]) + logrus.Warnf("Recovering from API handler panic: %v, %s", err, buf[:n]) // Try to inform client things went south... won't work if handler already started writing response body utils.InternalServerError(w, fmt.Errorf("%v", err)) } @@ -27,10 +29,23 @@ func (s *APIServer) APIHandler(h http.HandlerFunc) http.HandlerFunc { // Wrapper to hide some boiler plate fn := func(w http.ResponseWriter, r *http.Request) { - log.Debugf("APIHandler -- Method: %s URL: %s", r.Method, r.URL.String()) + rid := uuid.New().String() + if logrus.IsLevelEnabled(logrus.DebugLevel) { + logrus.Debugf("APIHandler(%s) -- Method: %s URL: %s", rid, r.Method, r.URL.String()) + for k, v := range r.Header { + switch auth.HeaderAuthName(k) { + case auth.XRegistryConfigHeader, auth.XRegistryAuthHeader: + logrus.Debugf("APIHandler(%s) -- Header: %s: <hidden>", rid, k) + default: + logrus.Debugf("APIHandler(%s) -- Header: %s: %v", rid, k, v) + } + } + } + // Set in case handler wishes to correlate logging events + r.Header.Set("X-Reference-Id", rid) if err := r.ParseForm(); err != nil { - log.Infof("Failed Request: unable to parse form: %q", err) + logrus.Infof("Failed Request: unable to parse form: %q (%s)", err, rid) } // TODO: Use r.ConnContext when ported to go 1.13 diff --git a/pkg/api/server/idle/tracker.go b/pkg/api/server/idle/tracker.go new file mode 100644 index 000000000..1b378c492 --- /dev/null +++ b/pkg/api/server/idle/tracker.go @@ -0,0 +1,96 @@ +package idle + +import ( + "net" + "net/http" + "sync" + "time" + + "github.com/sirupsen/logrus" +) + +// Tracker holds the state for the server's idle tracking +type Tracker struct { + // Duration is the API idle window + Duration time.Duration + hijacked int // count of active connections managed by handlers + managed map[net.Conn]struct{} // set of active connections managed by http package + mux sync.Mutex // protect managed map + timer *time.Timer + total int // total number of connections made to this server instance +} + +// NewTracker creates and initializes a new Tracker object +// For best behavior, duration should be 2x http idle connection timeout +func NewTracker(idle time.Duration) *Tracker { + return &Tracker{ + managed: make(map[net.Conn]struct{}), + Duration: idle, + timer: time.NewTimer(idle), + } +} + +// ConnState is called on HTTP connection state changes. +// - Once StateHijacked, StateClose is _NOT_ called on that connection +// - There are two "idle" timeouts, the http idle connection (not to be confused with the TCP/IP idle socket timeout) +// and the API idle window. The caller should set the http idle timeout to 2x the time provided to NewTacker() which +// is the API idle window. +func (t *Tracker) ConnState(conn net.Conn, state http.ConnState) { + t.mux.Lock() + defer t.mux.Unlock() + + logrus.Debugf("IdleTracker %p:%v %dm+%dh/%dt connection(s)", conn, state, len(t.managed), t.hijacked, t.TotalConnections()) + switch state { + case http.StateNew, http.StateActive: + // stop the API timer when the server transitions any connection to an "active" state + t.managed[conn] = struct{}{} + t.timer.Stop() + t.total++ + case http.StateHijacked: + // hijacked connections should call Close() when finished. + // Note: If a handler hijack's a connection and then doesn't Close() it, + // the API timer will not fire and the server will _NOT_ timeout. + delete(t.managed, conn) + t.hijacked++ + case http.StateIdle: + // When any connection goes into the http idle state, we know: + // - we have an active connection + // - the API timer should not be counting down (See case StateNew/StateActive) + break + case http.StateClosed: + oldActive := t.ActiveConnections() + + // Either the server or a hijacking handler has closed the http connection to a client + if _, found := t.managed[conn]; found { + delete(t.managed, conn) + } else { + t.hijacked-- // guarded by t.mux above + } + + // Transitioned from any "active" connection to no connections + if oldActive > 0 && t.ActiveConnections() == 0 { + t.timer.Stop() // See library source for Reset() issues and why they are not fixed + t.timer.Reset(t.Duration) // Restart the API window timer + } + } +} + +// Close is used to update Tracker that a StateHijacked connection has been closed by handler (StateClosed) +func (t *Tracker) Close() { + t.ConnState(nil, http.StateClosed) +} + +// ActiveConnections returns the number of current managed or StateHijacked connections +func (t *Tracker) ActiveConnections() int { + return len(t.managed) + t.hijacked +} + +// TotalConnections returns total number of connections made to this instance of the service +func (t *Tracker) TotalConnections() int { + return t.total +} + +// Done is called when idle timer has expired +func (t *Tracker) Done() <-chan time.Time { + return t.timer.C +} diff --git a/pkg/api/server/idletracker/idletracker.go b/pkg/api/server/idletracker/idletracker.go deleted file mode 100644 index 1ee905a99..000000000 --- a/pkg/api/server/idletracker/idletracker.go +++ /dev/null @@ -1,74 +0,0 @@ -package idletracker - -import ( - "net" - "net/http" - "sync" - "time" - - "github.com/sirupsen/logrus" -) - -type IdleTracker struct { - http map[net.Conn]struct{} - hijacked int - total int - mux sync.Mutex - timer *time.Timer - Duration time.Duration -} - -func NewIdleTracker(idle time.Duration) *IdleTracker { - return &IdleTracker{ - http: make(map[net.Conn]struct{}), - Duration: idle, - timer: time.NewTimer(idle), - } -} - -func (t *IdleTracker) ConnState(conn net.Conn, state http.ConnState) { - t.mux.Lock() - defer t.mux.Unlock() - - oldActive := t.ActiveConnections() - logrus.Debugf("IdleTracker %p:%v %d/%d connection(s)", conn, state, oldActive, t.TotalConnections()) - switch state { - case http.StateNew, http.StateActive: - t.http[conn] = struct{}{} - // stop the timer if we transitioned from idle - if oldActive == 0 { - t.timer.Stop() - } - t.total++ - case http.StateHijacked: - // hijacked connections are handled elsewhere - delete(t.http, conn) - t.hijacked++ - case http.StateIdle, http.StateClosed: - delete(t.http, conn) - // Restart the timer if we've become idle - if oldActive > 0 && len(t.http) == 0 { - t.timer.Stop() - t.timer.Reset(t.Duration) - } - } -} - -func (t *IdleTracker) TrackHijackedClosed() { - t.mux.Lock() - defer t.mux.Unlock() - - t.hijacked-- -} - -func (t *IdleTracker) ActiveConnections() int { - return len(t.http) + t.hijacked -} - -func (t *IdleTracker) TotalConnections() int { - return t.total -} - -func (t *IdleTracker) Done() <-chan time.Time { - return t.timer.C -} diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index b1007fe09..cb0d26d1e 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -1175,7 +1175,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // tags: // - images // summary: Untag an image - // description: Untag an image + // description: Untag an image. If not repo and tag are specified, all tags are removed from the image. // parameters: // - in: path // name: name:.* @@ -1423,6 +1423,13 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // description: | // output configuration TBD // (As of version 1.xx) + // - in: query + // name: httpproxy + // type: boolean + // default: + // description: | + // Inject http proxy environment variables into container + // (As of version 2.0.0) // produces: // - application/json // responses: diff --git a/pkg/api/server/register_volumes.go b/pkg/api/server/register_volumes.go index 22488b158..aa0f67604 100644 --- a/pkg/api/server/register_volumes.go +++ b/pkg/api/server/register_volumes.go @@ -154,7 +154,9 @@ func (s *APIServer) registerVolumeHandlers(r *mux.Router) error { // parameters: // - in: body // name: create - // description: attributes for creating a container + // description: | + // attributes for creating a container. + // Note: If a volume by the same name exists, a 201 response with that volume's information will be generated. // schema: // $ref: "#/definitions/DockerVolumeCreate" // produces: diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index e7c031234..09a9f6370 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -16,7 +16,7 @@ import ( "github.com/containers/podman/v2/libpod" "github.com/containers/podman/v2/pkg/api/handlers" - "github.com/containers/podman/v2/pkg/api/server/idletracker" + "github.com/containers/podman/v2/pkg/api/server/idle" "github.com/coreos/go-systemd/v22/activation" "github.com/coreos/go-systemd/v22/daemon" "github.com/gorilla/mux" @@ -26,14 +26,14 @@ import ( ) type APIServer struct { - http.Server // The HTTP work happens here - *schema.Decoder // Decoder for Query parameters to structs - context.Context // Context to carry objects to handlers - *libpod.Runtime // Where the real work happens - net.Listener // mux for routing HTTP API calls to libpod routines - context.CancelFunc // Stop APIServer - idleTracker *idletracker.IdleTracker // Track connections to support idle shutdown - pprof *http.Server // Sidecar http server for providing performance data + http.Server // The HTTP work happens here + *schema.Decoder // Decoder for Query parameters to structs + context.Context // Context to carry objects to handlers + *libpod.Runtime // Where the real work happens + net.Listener // mux for routing HTTP API calls to libpod routines + context.CancelFunc // Stop APIServer + idleTracker *idle.Tracker // Track connections to support idle shutdown + pprof *http.Server // Sidecar http server for providing performance data } // Number of seconds to wait for next request, if exceeded shutdown server @@ -70,13 +70,13 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li } router := mux.NewRouter().UseEncodedPath() - idle := idletracker.NewIdleTracker(duration) + idle := idle.NewTracker(duration) server := APIServer{ Server: http.Server{ Handler: router, ReadHeaderTimeout: 20 * time.Second, - IdleTimeout: duration, + IdleTimeout: duration * 2, ConnState: idle.ConnState, ErrorLog: log.New(logrus.StandardLogger().Out, "", 0), }, diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 69a7da869..fcbf6fe39 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -3,6 +3,7 @@ package auth import ( "encoding/base64" "encoding/json" + "fmt" "io/ioutil" "net/http" "os" @@ -15,21 +16,98 @@ import ( "github.com/sirupsen/logrus" ) -// XRegistryAuthHeader is the key to the encoded registry authentication -// configuration in an http-request header. -const XRegistryAuthHeader = "X-Registry-Auth" +type HeaderAuthName string -// GetCredentials extracts one or more DockerAuthConfigs from the request's +func (h HeaderAuthName) String() string { return string(h) } + +// XRegistryAuthHeader is the key to the encoded registry authentication configuration in an http-request header. +// This header supports one registry per header occurrence. To support N registries provided N headers, one per registry. +// As of Docker API 1.40 and Libpod API 1.0.0, this header is supported by all endpoints. +const XRegistryAuthHeader HeaderAuthName = "X-Registry-Auth" + +// XRegistryConfigHeader is the key to the encoded registry authentication configuration in an http-request header. +// This header supports N registries in one header via a Base64 encoded, JSON map. +// As of Docker API 1.40 and Libpod API 2.0.0, this header is supported by build endpoints. +const XRegistryConfigHeader HeaderAuthName = "X-Registry-Config" + +// GetCredentials queries the http.Request for X-Registry-.* headers and extracts +// the necessary authentication information for libpod operations +func GetCredentials(r *http.Request) (*types.DockerAuthConfig, string, HeaderAuthName, error) { + has := func(key HeaderAuthName) bool { hdr, found := r.Header[string(key)]; return found && len(hdr) > 0 } + switch { + case has(XRegistryConfigHeader): + c, f, err := getConfigCredentials(r) + return c, f, XRegistryConfigHeader, err + case has(XRegistryAuthHeader): + c, f, err := getAuthCredentials(r) + return c, f, XRegistryAuthHeader, err + + } + return nil, "", "", nil +} + +// getConfigCredentials extracts one or more docker.AuthConfig from the request's +// header. An empty key will be used as default while a named registry will be +// returned as types.DockerAuthConfig +func getConfigCredentials(r *http.Request) (*types.DockerAuthConfig, string, error) { + var auth *types.DockerAuthConfig + configs := make(map[string]types.DockerAuthConfig) + + for _, h := range r.Header[string(XRegistryConfigHeader)] { + param, err := base64.URLEncoding.DecodeString(h) + if err != nil { + return nil, "", errors.Wrapf(err, "failed to decode %q", XRegistryConfigHeader) + } + + ac := make(map[string]dockerAPITypes.AuthConfig) + err = json.Unmarshal(param, &ac) + if err != nil { + return nil, "", errors.Wrapf(err, "failed to unmarshal %q", XRegistryConfigHeader) + } + + for k, v := range ac { + configs[k] = dockerAuthToImageAuth(v) + } + } + + // Empty key implies no registry given in API + if c, found := configs[""]; found { + auth = &c + } + + // Override any default given above if specialized credentials provided + if registries, found := r.URL.Query()["registry"]; found { + for _, r := range registries { + for k, v := range configs { + if strings.Contains(k, r) { + v := v + auth = &v + break + } + } + if auth != nil { + break + } + } + + if auth == nil { + logrus.Debugf("%q header found in request, but \"registry=%v\" query parameter not provided", + XRegistryConfigHeader, registries) + } else { + logrus.Debugf("%q header found in request for username %q", XRegistryConfigHeader, auth.Username) + } + } + + authfile, err := authConfigsToAuthFile(configs) + return auth, authfile, err +} + +// getAuthCredentials extracts one or more DockerAuthConfigs from the request's // header. The header could specify a single-auth config in which case the // first return value is set. In case of a multi-auth header, the contents are // stored in a temporary auth file (2nd return value). Note that the auth file // should be removed after usage. -func GetCredentials(r *http.Request) (*types.DockerAuthConfig, string, error) { - authHeader := r.Header.Get(XRegistryAuthHeader) - if len(authHeader) == 0 { - return nil, "", nil - } - +func getAuthCredentials(r *http.Request) (*types.DockerAuthConfig, string, error) { // First look for a multi-auth header (i.e., a map). authConfigs, err := multiAuthHeader(r) if err == nil { @@ -51,38 +129,75 @@ func GetCredentials(r *http.Request) (*types.DockerAuthConfig, string, error) { return conf, "", nil } -// Header returns a map with the XRegistryAuthHeader set which can +// Header builds the requested Authentication Header +func Header(sys *types.SystemContext, headerName HeaderAuthName, authfile, username, password string) (map[string]string, error) { + var ( + content string + err error + ) + switch headerName { + case XRegistryAuthHeader: + content, err = headerAuth(sys, authfile, username, password) + case XRegistryConfigHeader: + content, err = headerConfig(sys, authfile, username, password) + default: + err = fmt.Errorf("unsupported authentication header: %q", headerName) + } + if err != nil { + return nil, err + } + + if len(content) > 0 { + return map[string]string{string(headerName): content}, nil + } + return nil, nil +} + +// headerConfig returns a map with the XRegistryConfigHeader set which can // conveniently be used in the http stack. -func Header(sys *types.SystemContext, authfile, username, password string) (map[string]string, error) { - var content string - var err error +func headerConfig(sys *types.SystemContext, authfile, username, password string) (string, error) { + if sys == nil { + sys = &types.SystemContext{} + } + if authfile != "" { + sys.AuthFilePath = authfile + } + authConfigs, err := imageAuth.GetAllCredentials(sys) + if err != nil { + return "", err + } if username != "" { - content, err = encodeSingleAuthConfig(types.DockerAuthConfig{Username: username, Password: password}) - if err != nil { - return nil, err - } - } else { - if sys == nil { - sys = &types.SystemContext{} - } - if authfile != "" { - sys.AuthFilePath = authfile - } - authConfigs, err := imageAuth.GetAllCredentials(sys) - if err != nil { - return nil, err - } - content, err = encodeMultiAuthConfigs(authConfigs) - if err != nil { - return nil, err + authConfigs[""] = types.DockerAuthConfig{ + Username: username, + Password: password, } } - header := make(map[string]string) - header[XRegistryAuthHeader] = content + if len(authConfigs) == 0 { + return "", nil + } + return encodeMultiAuthConfigs(authConfigs) +} - return header, nil +// headerAuth returns a base64 encoded map with the XRegistryAuthHeader set which can +// conveniently be used in the http stack. +func headerAuth(sys *types.SystemContext, authfile, username, password string) (string, error) { + if username != "" { + return encodeSingleAuthConfig(types.DockerAuthConfig{Username: username, Password: password}) + } + + if sys == nil { + sys = &types.SystemContext{} + } + if authfile != "" { + sys.AuthFilePath = authfile + } + authConfigs, err := imageAuth.GetAllCredentials(sys) + if err != nil { + return "", err + } + return encodeMultiAuthConfigs(authConfigs) } // RemoveAuthfile is a convenience function that is meant to be called in a @@ -180,7 +295,7 @@ func imageAuthToDockerAuth(authConfig types.DockerAuthConfig) dockerAPITypes.Aut // singleAuthHeader extracts a DockerAuthConfig from the request's header. // The header content is a single DockerAuthConfig. func singleAuthHeader(r *http.Request) (map[string]types.DockerAuthConfig, error) { - authHeader := r.Header.Get(XRegistryAuthHeader) + authHeader := r.Header.Get(string(XRegistryAuthHeader)) authConfig := dockerAPITypes.AuthConfig{} if len(authHeader) > 0 { authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authHeader)) @@ -196,7 +311,7 @@ func singleAuthHeader(r *http.Request) (map[string]types.DockerAuthConfig, error // multiAuthHeader extracts a DockerAuthConfig from the request's header. // The header content is a map[string]DockerAuthConfigs. func multiAuthHeader(r *http.Request) (map[string]types.DockerAuthConfig, error) { - authHeader := r.Header.Get(XRegistryAuthHeader) + authHeader := r.Header.Get(string(XRegistryAuthHeader)) if len(authHeader) == 0 { return nil, nil } diff --git a/pkg/bindings/images/build.go b/pkg/bindings/images/build.go index 9082670a7..60ffea548 100644 --- a/pkg/bindings/images/build.go +++ b/pkg/bindings/images/build.go @@ -15,6 +15,7 @@ import ( "strings" "github.com/containers/buildah" + "github.com/containers/podman/v2/pkg/auth" "github.com/containers/podman/v2/pkg/bindings" "github.com/containers/podman/v2/pkg/domain/entities" "github.com/docker/go-units" @@ -60,7 +61,7 @@ func Build(ctx context.Context, containerFiles []string, options entities.BuildO params.Set("cpushares", strconv.Itoa(int(cpuShares))) } if cpuSetCpus := options.CommonBuildOpts.CPUSetCPUs; len(cpuSetCpus) > 0 { - params.Set("cpusetcpues", cpuSetCpus) + params.Set("cpusetcpus", cpuSetCpus) } if cpuPeriod := options.CommonBuildOpts.CPUPeriod; cpuPeriod > 0 { params.Set("cpuperiod", strconv.Itoa(int(cpuPeriod))) @@ -92,6 +93,26 @@ func Build(ctx context.Context, containerFiles []string, options entities.BuildO } params.Set("labels", l) } + if options.CommonBuildOpts.HTTPProxy { + params.Set("httpproxy", "1") + } + + var ( + headers map[string]string + err error + ) + if options.SystemContext == nil { + headers, err = auth.Header(options.SystemContext, auth.XRegistryConfigHeader, "", "", "") + } else { + if options.SystemContext.DockerAuthConfig != nil { + headers, err = auth.Header(options.SystemContext, auth.XRegistryAuthHeader, options.SystemContext.AuthFilePath, options.SystemContext.DockerAuthConfig.Username, options.SystemContext.DockerAuthConfig.Password) + } else { + headers, err = auth.Header(options.SystemContext, auth.XRegistryConfigHeader, options.SystemContext.AuthFilePath, "", "") + } + } + if err != nil { + return nil, err + } stdout := io.Writer(os.Stdout) if options.Out != nil { @@ -125,7 +146,7 @@ func Build(ctx context.Context, containerFiles []string, options entities.BuildO if err != nil { return nil, err } - response, err := conn.DoRequest(tarfile, http.MethodPost, "/build", params, nil) + response, err := conn.DoRequest(tarfile, http.MethodPost, "/build", params, headers) if err != nil { return nil, err } diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go index 596491044..a78e7f4c6 100644 --- a/pkg/bindings/images/images.go +++ b/pkg/bindings/images/images.go @@ -282,7 +282,7 @@ func Push(ctx context.Context, source string, destination string, options entiti } // TODO: have a global system context we can pass around (1st argument) - header, err := auth.Header(nil, options.Authfile, options.Username, options.Password) + header, err := auth.Header(nil, auth.XRegistryAuthHeader, options.Authfile, options.Username, options.Password) if err != nil { return err } @@ -325,7 +325,7 @@ func Search(ctx context.Context, term string, opts entities.ImageSearchOptions) } // TODO: have a global system context we can pass around (1st argument) - header, err := auth.Header(nil, opts.Authfile, "", "") + header, err := auth.Header(nil, auth.XRegistryAuthHeader, opts.Authfile, "", "") if err != nil { return nil, err } diff --git a/pkg/bindings/images/pull.go b/pkg/bindings/images/pull.go index 2bfbbb2ac..c827b3283 100644 --- a/pkg/bindings/images/pull.go +++ b/pkg/bindings/images/pull.go @@ -42,7 +42,7 @@ func Pull(ctx context.Context, rawImage string, options entities.ImagePullOption params.Set("allTags", strconv.FormatBool(options.AllTags)) // TODO: have a global system context we can pass around (1st argument) - header, err := auth.Header(nil, options.Authfile, options.Username, options.Password) + header, err := auth.Header(nil, auth.XRegistryAuthHeader, options.Authfile, options.Username, options.Password) if err != nil { return nil, err } diff --git a/pkg/bindings/play/play.go b/pkg/bindings/play/play.go index 32f9bb4a9..ffaee3208 100644 --- a/pkg/bindings/play/play.go +++ b/pkg/bindings/play/play.go @@ -33,7 +33,7 @@ func Kube(ctx context.Context, path string, options entities.PlayKubeOptions) (* } // TODO: have a global system context we can pass around (1st argument) - header, err := auth.Header(nil, options.Authfile, options.Username, options.Password) + header, err := auth.Header(nil, auth.XRegistryAuthHeader, options.Authfile, options.Username, options.Password) if err != nil { return nil, err } diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index f105dc333..803a59932 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -78,6 +78,6 @@ type ContainerEngine interface { VolumeCreate(ctx context.Context, opts VolumeCreateOptions) (*IDOrNameResponse, error) VolumeInspect(ctx context.Context, namesOrIds []string, opts VolumeInspectOptions) ([]*VolumeInspectReport, error) VolumeList(ctx context.Context, opts VolumeListOptions) ([]*VolumeListReport, error) - VolumePrune(ctx context.Context, opts VolumePruneOptions) ([]*VolumePruneReport, error) + VolumePrune(ctx context.Context) ([]*VolumePruneReport, error) VolumeRm(ctx context.Context, namesOrIds []string, opts VolumeRmOptions) ([]*VolumeRmReport, error) } diff --git a/pkg/domain/entities/pods.go b/pkg/domain/entities/pods.go index 7b38dbd87..426419833 100644 --- a/pkg/domain/entities/pods.go +++ b/pkg/domain/entities/pods.go @@ -142,6 +142,7 @@ func (p PodCreateOptions) ToPodSpecGen(s *specgen.PodSpecGenerator) { s.StaticMAC = p.Net.StaticMAC s.PortMappings = p.Net.PublishPorts s.CNINetworks = p.Net.CNINetworks + s.NetworkOptions = p.Net.NetworkOptions if p.Net.UseImageResolvConf { s.NoManageResolvConf = true } diff --git a/pkg/domain/entities/volumes.go b/pkg/domain/entities/volumes.go index 53d30ffdf..fb8466d04 100644 --- a/pkg/domain/entities/volumes.go +++ b/pkg/domain/entities/volumes.go @@ -113,10 +113,6 @@ type VolumeInspectReport struct { *VolumeConfigResponse } -type VolumePruneOptions struct { - Force bool -} - type VolumePruneReport struct { Err error Id string //nolint diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index d56dc7d94..965c63bec 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -46,11 +46,7 @@ func (ir *ImageEngine) Exists(_ context.Context, nameOrID string) (*entities.Boo } func (ir *ImageEngine) Prune(ctx context.Context, opts entities.ImagePruneOptions) (*entities.ImagePruneReport, error) { - return ir.pruneImagesHelper(ctx, opts.All, opts.Filter) -} - -func (ir *ImageEngine) pruneImagesHelper(ctx context.Context, all bool, filters []string) (*entities.ImagePruneReport, error) { - results, err := ir.Libpod.ImageRuntime().PruneImages(ctx, all, filters) + results, err := ir.Libpod.ImageRuntime().PruneImages(ctx, opts.All, opts.Filter) if err != nil { return nil, err } diff --git a/pkg/domain/infra/abi/manifest.go b/pkg/domain/infra/abi/manifest.go index 672d0a69f..6c518e678 100644 --- a/pkg/domain/infra/abi/manifest.go +++ b/pkg/domain/infra/abi/manifest.go @@ -3,6 +3,7 @@ package abi import ( + "bytes" "context" "encoding/json" "fmt" @@ -11,15 +12,17 @@ import ( "strings" "github.com/containers/buildah/manifests" + buildahManifests "github.com/containers/buildah/pkg/manifests" + "github.com/containers/buildah/util" buildahUtil "github.com/containers/buildah/util" cp "github.com/containers/image/v5/copy" "github.com/containers/image/v5/docker" "github.com/containers/image/v5/manifest" + "github.com/containers/image/v5/transports" "github.com/containers/image/v5/transports/alltransports" "github.com/containers/image/v5/types" libpodImage "github.com/containers/podman/v2/libpod/image" "github.com/containers/podman/v2/pkg/domain/entities" - "github.com/containers/podman/v2/pkg/util" "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -41,28 +44,82 @@ func (ir *ImageEngine) ManifestCreate(ctx context.Context, names, images []strin // ManifestInspect returns the content of a manifest list or image func (ir *ImageEngine) ManifestInspect(ctx context.Context, name string) ([]byte, error) { - dockerPrefix := fmt.Sprintf("%s://", docker.Transport.Name()) - _, err := alltransports.ParseImageName(name) + if newImage, err := ir.Libpod.ImageRuntime().NewFromLocal(name); err == nil { + // return the manifest in local storage + if list, err := newImage.InspectManifest(); err == nil { + buf, err := json.MarshalIndent(list, "", " ") + if err != nil { + return buf, errors.Wrapf(err, "error rendering manifest %s for display", name) + } + return buf, nil + // no return if local image is not a list of images type + // continue on getting valid manifest through remote serice + } else if errors.Cause(err) != buildahManifests.ErrManifestTypeNotSupported { + return nil, errors.Wrapf(err, "loading manifest %q", name) + } + } + sc := ir.Libpod.SystemContext() + refs, err := util.ResolveNameToReferences(ir.Libpod.GetStore(), sc, name) if err != nil { - _, err = alltransports.ParseImageName(dockerPrefix + name) + return nil, err + } + var ( + latestErr error + result []byte + manType string + b bytes.Buffer + ) + appendErr := func(e error) { + if latestErr == nil { + latestErr = e + } else { + latestErr = errors.Wrapf(latestErr, "tried %v\n", e) + } + } + for _, ref := range refs { + src, err := ref.NewImageSource(ctx, sc) + if err != nil { + appendErr(errors.Wrapf(err, "reading image %q", transports.ImageName(ref))) + continue + } + defer src.Close() + + manifestBytes, manifestType, err := src.GetManifest(ctx, nil) if err != nil { - return nil, errors.Errorf("invalid image reference %q", name) + appendErr(errors.Wrapf(err, "loading manifest %q", transports.ImageName(ref))) + continue } + + if !manifest.MIMETypeIsMultiImage(manifestType) { + appendErr(errors.Errorf("manifest is of type %s (not a list type)", manifestType)) + continue + } + result = manifestBytes + manType = manifestType + break } - image, err := ir.Libpod.ImageRuntime().New(ctx, name, "", "", nil, nil, libpodImage.SigningOptions{}, nil, util.PullImageMissing) - if err != nil { - return nil, errors.Wrapf(err, "reading image %q", name) + if len(result) == 0 && latestErr != nil { + return nil, latestErr } + if manType != manifest.DockerV2ListMediaType { + listBlob, err := manifest.ListFromBlob(result, manType) + if err != nil { + return nil, errors.Wrapf(err, "error parsing manifest blob %q as a %q", string(result), manType) + } + list, err := listBlob.ConvertToMIMEType(manifest.DockerV2ListMediaType) + if err != nil { + return nil, err + } + if result, err = list.Serialize(); err != nil { + return nil, err + } - list, err := image.InspectManifest() - if err != nil { - return nil, errors.Wrapf(err, "loading manifest %q", name) } - buf, err := json.MarshalIndent(list, "", " ") + err = json.Indent(&b, result, "", " ") if err != nil { - return buf, errors.Wrapf(err, "error rendering manifest for display") + return nil, errors.Wrapf(err, "error rendering manifest %s for display", name) } - return buf, nil + return b.Bytes(), nil } // ManifestAdd adds images to the manifest list diff --git a/pkg/domain/infra/abi/network.go b/pkg/domain/infra/abi/network.go index 053be6528..5acfea853 100644 --- a/pkg/domain/infra/abi/network.go +++ b/pkg/domain/infra/abi/network.go @@ -12,6 +12,7 @@ import ( "github.com/containernetworking/cni/libcni" cniversion "github.com/containernetworking/cni/pkg/version" "github.com/containers/podman/v2/libpod" + "github.com/containers/podman/v2/libpod/define" "github.com/containers/podman/v2/pkg/domain/entities" "github.com/containers/podman/v2/pkg/network" "github.com/containers/podman/v2/pkg/util" @@ -85,7 +86,7 @@ func (ic *ContainerEngine) NetworkRm(ctx context.Context, namesOrIds []string, o // if user passes force, we nuke containers and pods if !options.Force { // Without the force option, we return an error - return reports, errors.Errorf("%q has associated containers with it. Use -f to forcibly delete containers and pods", name) + return reports, errors.Wrapf(define.ErrNetworkInUse, "%q has associated containers with it. Use -f to forcibly delete containers and pods", name) } if c.IsInfra() { // if we have a infra container we need to remove the pod diff --git a/pkg/domain/infra/abi/volumes.go b/pkg/domain/infra/abi/volumes.go index 340f00953..946f258af 100644 --- a/pkg/domain/infra/abi/volumes.go +++ b/pkg/domain/infra/abi/volumes.go @@ -120,7 +120,7 @@ func (ic *ContainerEngine) VolumeInspect(ctx context.Context, namesOrIds []strin return reports, nil } -func (ic *ContainerEngine) VolumePrune(ctx context.Context, opts entities.VolumePruneOptions) ([]*entities.VolumePruneReport, error) { +func (ic *ContainerEngine) VolumePrune(ctx context.Context) ([]*entities.VolumePruneReport, error) { return ic.pruneVolumesHelper(ctx) } diff --git a/pkg/domain/infra/tunnel/helpers.go b/pkg/domain/infra/tunnel/helpers.go index 5944f855a..7d28f1bd3 100644 --- a/pkg/domain/infra/tunnel/helpers.go +++ b/pkg/domain/infra/tunnel/helpers.go @@ -2,74 +2,118 @@ package tunnel import ( "context" - "strings" "github.com/containers/podman/v2/libpod/define" "github.com/containers/podman/v2/pkg/bindings" "github.com/containers/podman/v2/pkg/bindings/containers" "github.com/containers/podman/v2/pkg/bindings/pods" "github.com/containers/podman/v2/pkg/domain/entities" - "github.com/containers/podman/v2/pkg/util" + "github.com/containers/podman/v2/pkg/errorhandling" "github.com/pkg/errors" ) +// FIXME: the `ignore` parameter is very likely wrong here as it should rather +// be used on *errors* from operations such as remove. func getContainersByContext(contextWithConnection context.Context, all, ignore bool, namesOrIDs []string) ([]entities.ListContainer, error) { - var ( - cons []entities.ListContainer - ) if all && len(namesOrIDs) > 0 { return nil, errors.New("cannot lookup containers and all") } - c, err := containers.List(contextWithConnection, nil, bindings.PTrue, nil, nil, bindings.PTrue) + + allContainers, err := containers.List(contextWithConnection, nil, bindings.PTrue, nil, nil, bindings.PTrue) if err != nil { return nil, err } if all { - return c, err + return allContainers, err } - for _, id := range namesOrIDs { - var found bool - for _, con := range c { - if id == con.ID || strings.HasPrefix(con.ID, id) || util.StringInSlice(id, con.Names) { - cons = append(cons, con) + + // Note: it would be nicer if the lists endpoint would support that as + // we could use the libpod backend for looking up containers rather + // than risking diverging the local and remote lookups. + // + // A `--filter nameOrId=abc` that can be specified multiple times would + // be awesome to have. + filtered := []entities.ListContainer{} + for _, nameOrID := range namesOrIDs { + // First determine if the container exists by doing an inspect. + // Inspect takes supports names and IDs and let's us determine + // a containers full ID. + inspectData, err := containers.Inspect(contextWithConnection, nameOrID, bindings.PFalse) + if err != nil { + if ignore && errorhandling.Contains(err, define.ErrNoSuchCtr) { + continue + } + return nil, err + } + + // Now we can do a full match of the ID to find the right + // container. Note that we *really* need a full ID match to + // prevent any ambiguities between IDs and names (see #7837). + found := false + for _, ctr := range allContainers { + if ctr.ID == inspectData.ID { + filtered = append(filtered, ctr) found = true break } + } + if !found && !ignore { - return nil, errors.Wrapf(define.ErrNoSuchCtr, "unable to find container %q", id) + return nil, errors.Wrapf(define.ErrNoSuchCtr, "unable to find container %q", nameOrID) } } - return cons, nil + return filtered, nil } func getPodsByContext(contextWithConnection context.Context, all bool, namesOrIDs []string) ([]*entities.ListPodsReport, error) { - var ( - sPods []*entities.ListPodsReport - ) if all && len(namesOrIDs) > 0 { return nil, errors.New("cannot lookup specific pods and all") } - fPods, err := pods.List(contextWithConnection, nil) + allPods, err := pods.List(contextWithConnection, nil) if err != nil { return nil, err } if all { - return fPods, nil + return allPods, nil } + + filtered := []*entities.ListPodsReport{} + // Note: it would be nicer if the lists endpoint would support that as + // we could use the libpod backend for looking up pods rather than + // risking diverging the local and remote lookups. + // + // A `--filter nameOrId=abc` that can be specified multiple times would + // be awesome to have. for _, nameOrID := range namesOrIDs { - var found bool - for _, f := range fPods { - if f.Name == nameOrID || strings.HasPrefix(f.Id, nameOrID) { - sPods = append(sPods, f) + // First determine if the pod exists by doing an inspect. + // Inspect takes supports names and IDs and let's us determine + // a containers full ID. + inspectData, err := pods.Inspect(contextWithConnection, nameOrID) + if err != nil { + if errorhandling.Contains(err, define.ErrNoSuchPod) { + return nil, errors.Wrapf(define.ErrNoSuchPod, "unable to find pod %q", nameOrID) + } + return nil, err + } + + // Now we can do a full match of the ID to find the right pod. + // Note that we *really* need a full ID match to prevent any + // ambiguities between IDs and names (see #7837). + found := false + for _, pod := range allPods { + if pod.Id == inspectData.ID { + filtered = append(filtered, pod) found = true break } + } + if !found { return nil, errors.Wrapf(define.ErrNoSuchPod, "unable to find pod %q", nameOrID) } } - return sPods, nil + return filtered, nil } diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index 981884109..61ac2141c 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -9,7 +9,6 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/image/v5/docker/reference" - "github.com/containers/podman/v2/pkg/bindings" images "github.com/containers/podman/v2/pkg/bindings/images" "github.com/containers/podman/v2/pkg/domain/entities" "github.com/containers/podman/v2/pkg/domain/utils" @@ -139,13 +138,8 @@ func (ir *ImageEngine) Tag(ctx context.Context, nameOrID string, tags []string, } func (ir *ImageEngine) Untag(ctx context.Context, nameOrID string, tags []string, options entities.ImageUntagOptions) error { - // Remove all tags if none are provided if len(tags) == 0 { - newImage, err := images.GetImage(ir.ClientCxt, nameOrID, bindings.PFalse) - if err != nil { - return err - } - tags = newImage.NamesHistory + return images.Untag(ir.ClientCxt, nameOrID, "", "") } for _, newTag := range tags { diff --git a/pkg/domain/infra/tunnel/volumes.go b/pkg/domain/infra/tunnel/volumes.go index ee2786330..e432d3292 100644 --- a/pkg/domain/infra/tunnel/volumes.go +++ b/pkg/domain/infra/tunnel/volumes.go @@ -56,7 +56,7 @@ func (ic *ContainerEngine) VolumeInspect(ctx context.Context, namesOrIds []strin return reports, nil } -func (ic *ContainerEngine) VolumePrune(ctx context.Context, opts entities.VolumePruneOptions) ([]*entities.VolumePruneReport, error) { +func (ic *ContainerEngine) VolumePrune(ctx context.Context) ([]*entities.VolumePruneReport, error) { return volumes.Prune(ic.ClientCxt) } diff --git a/pkg/registries/registries.go b/pkg/registries/registries.go index 5dff25c7d..949c5d835 100644 --- a/pkg/registries/registries.go +++ b/pkg/registries/registries.go @@ -1,5 +1,10 @@ package registries +// TODO: this package should not exist anymore. Users should either use +// c/image's `sysregistriesv2` package directly OR, even better, we cache a +// config in libpod's image runtime so we don't need to parse the +// registries.conf files redundantly. + import ( "os" "path/filepath" diff --git a/pkg/spec/config_linux.go b/pkg/spec/config_linux.go index d03663f12..319cce61f 100644 --- a/pkg/spec/config_linux.go +++ b/pkg/spec/config_linux.go @@ -200,6 +200,9 @@ func getDevices(path string) ([]*configs.Device, error) { } case f.Name() == "console": continue + case f.Mode()&os.ModeSymlink != 0: + // do not add symlink'd devices to privileged devices + continue } device, err := devices.DeviceFromPath(filepath.Join(path, f.Name()), "rwm") if err != nil { diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index 42228540c..81620997f 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -182,14 +182,23 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM g.SetProcessCwd(config.WorkDir) ProcessArgs := make([]string, 0) - if len(config.Entrypoint) > 0 { - ProcessArgs = config.Entrypoint + // We need to iterate the input for entrypoint because it is a []string + // but "" is a legit json input, which translates into a []string with an + // empty position. This messes up the eventual command being executed + // in the container + for _, a := range config.Entrypoint { + if len(a) > 0 { + ProcessArgs = append(ProcessArgs, a) + } } - if len(config.Command) > 0 { - ProcessArgs = append(ProcessArgs, config.Command...) + // Same issue as explained above for config.Entrypoint. + for _, a := range config.Command { + if len(a) > 0 { + ProcessArgs = append(ProcessArgs, a) + } } - g.SetProcessArgs(ProcessArgs) + g.SetProcessArgs(ProcessArgs) g.SetProcessTerminal(config.Tty) for key, val := range config.Annotations { diff --git a/pkg/specgen/generate/config_linux.go b/pkg/specgen/generate/config_linux.go index 1d5dcd8e7..fac02ad01 100644 --- a/pkg/specgen/generate/config_linux.go +++ b/pkg/specgen/generate/config_linux.go @@ -248,6 +248,13 @@ func addDevice(g *generate.Generator, device string) error { } g.Config.Mounts = append(g.Config.Mounts, devMnt) return nil + } else if src == "/dev/fuse" { + // if the user is asking for fuse inside the container + // make sure the module is loaded. + f, err := unix.Open(src, unix.O_RDONLY|unix.O_NONBLOCK, 0) + if err == nil { + unix.Close(f) + } } dev.Path = dst g.AddDevice(*dev) diff --git a/pkg/specgen/generate/container.go b/pkg/specgen/generate/container.go index 147ebd61b..2ee8f2441 100644 --- a/pkg/specgen/generate/container.go +++ b/pkg/specgen/generate/container.go @@ -13,6 +13,7 @@ import ( "github.com/containers/podman/v2/pkg/specgen" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "golang.org/x/sys/unix" ) @@ -33,7 +34,43 @@ func CompleteSpec(ctx context.Context, r *libpod.Runtime, s *specgen.SpecGenerat _, mediaType, err := newImage.Manifest(ctx) if err != nil { - return nil, err + if errors.Cause(err) != image.ErrImageIsBareList { + return nil, err + } + // if err is not runnable image + // use the local store image with repo@digest matches with the list, if exists + manifestByte, manifestType, err := newImage.GetManifest(ctx, nil) + if err != nil { + return nil, err + } + list, err := manifest.ListFromBlob(manifestByte, manifestType) + if err != nil { + return nil, err + } + images, err := r.ImageRuntime().GetImages() + if err != nil { + return nil, err + } + findLocal := false + listDigest, err := list.ChooseInstance(r.SystemContext()) + if err != nil { + return nil, err + } + for _, img := range images { + for _, imageDigest := range img.Digests() { + if imageDigest == listDigest { + newImage = img + s.Image = img.ID() + mediaType = manifestType + findLocal = true + logrus.Debug("image contains manifest list, using image from local storage") + break + } + } + } + if !findLocal { + return nil, image.ErrImageIsBareList + } } if s.HealthConfig == nil && mediaType == manifest.DockerV2Schema2MediaType { @@ -75,8 +112,8 @@ func CompleteSpec(ctx context.Context, r *libpod.Runtime, s *specgen.SpecGenerat if err != nil { return nil, errors.Wrap(err, "error parsing fields in containers.conf") } - if defaultEnvs["containers"] == "" { - defaultEnvs["containers"] = "podman" + if defaultEnvs["container"] == "" { + defaultEnvs["container"] = "podman" } var envs map[string]string diff --git a/pkg/specgen/generate/pod_create.go b/pkg/specgen/generate/pod_create.go index 101201252..43caf0fe9 100644 --- a/pkg/specgen/generate/pod_create.go +++ b/pkg/specgen/generate/pod_create.go @@ -99,6 +99,9 @@ func createPodOptions(p *specgen.PodSpecGenerator, rt *libpod.Runtime) ([]libpod case specgen.Host: logrus.Debugf("Pod will use host networking") options = append(options, libpod.WithPodHostNetwork()) + case specgen.Slirp: + logrus.Debugf("Pod will use slirp4netns") + options = append(options, libpod.WithPodSlirp4netns(p.NetworkOptions)) default: return nil, errors.Errorf("pods presently do not support network mode %s", p.NetNS.NSMode) } diff --git a/pkg/specgen/generate/security.go b/pkg/specgen/generate/security.go index 87e8029a7..d17cd4a9a 100644 --- a/pkg/specgen/generate/security.go +++ b/pkg/specgen/generate/security.go @@ -7,6 +7,7 @@ import ( "github.com/containers/common/pkg/capabilities" "github.com/containers/common/pkg/config" "github.com/containers/podman/v2/libpod" + "github.com/containers/podman/v2/libpod/define" "github.com/containers/podman/v2/libpod/image" "github.com/containers/podman/v2/pkg/specgen" "github.com/containers/podman/v2/pkg/util" @@ -130,12 +131,13 @@ func securityConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator, } configSpec := g.Config + configSpec.Process.Capabilities.Ambient = []string{} configSpec.Process.Capabilities.Bounding = caplist + configSpec.Process.Capabilities.Inheritable = caplist if s.User == "" || s.User == "root" || s.User == "0" { configSpec.Process.Capabilities.Effective = caplist configSpec.Process.Capabilities.Permitted = caplist - configSpec.Process.Capabilities.Inheritable = caplist } else { userCaps, err := capabilities.NormalizeCapabilities(s.CapAdd) if err != nil { @@ -167,7 +169,52 @@ func securityConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator, } g.SetRootReadonly(s.ReadOnlyFilesystem) + + // Add default sysctls + defaultSysctls, err := util.ValidateSysctls(rtc.Sysctls()) + if err != nil { + return err + } + for sysctlKey, sysctlVal := range defaultSysctls { + + // Ignore mqueue sysctls if --ipc=host + if s.IpcNS.IsHost() && strings.HasPrefix(sysctlKey, "fs.mqueue.") { + logrus.Infof("Sysctl %s=%s ignored in containers.conf, since IPC Namespace set to host", sysctlKey, sysctlVal) + + continue + } + + // Ignore net sysctls if --net=host + if s.NetNS.IsHost() && strings.HasPrefix(sysctlKey, "net.") { + logrus.Infof("Sysctl %s=%s ignored in containers.conf, since Network Namespace set to host", sysctlKey, sysctlVal) + continue + } + + // Ignore uts sysctls if --uts=host + if s.UtsNS.IsHost() && (strings.HasPrefix(sysctlKey, "kernel.domainname") || strings.HasPrefix(sysctlKey, "kernel.hostname")) { + logrus.Infof("Sysctl %s=%s ignored in containers.conf, since UTS Namespace set to host", sysctlKey, sysctlVal) + continue + } + + g.AddLinuxSysctl(sysctlKey, sysctlVal) + } + for sysctlKey, sysctlVal := range s.Sysctl { + + if s.IpcNS.IsHost() && strings.HasPrefix(sysctlKey, "fs.mqueue.") { + return errors.Wrapf(define.ErrInvalidArg, "sysctl %s=%s can't be set since IPC Namespace set to host", sysctlKey, sysctlVal) + } + + // Ignore net sysctls if --net=host + if s.NetNS.IsHost() && strings.HasPrefix(sysctlKey, "net.") { + return errors.Wrapf(define.ErrInvalidArg, "sysctl %s=%s can't be set since Host Namespace set to host", sysctlKey, sysctlVal) + } + + // Ignore uts sysctls if --uts=host + if s.UtsNS.IsHost() && (strings.HasPrefix(sysctlKey, "kernel.domainname") || strings.HasPrefix(sysctlKey, "kernel.hostname")) { + return errors.Wrapf(define.ErrInvalidArg, "sysctl %s=%s can't be set since UTS Namespace set to host", sysctlKey, sysctlVal) + } + g.AddLinuxSysctl(sysctlKey, sysctlVal) } diff --git a/pkg/specgen/pod_validate.go b/pkg/specgen/pod_validate.go index 907c0bb69..a6c61a203 100644 --- a/pkg/specgen/pod_validate.go +++ b/pkg/specgen/pod_validate.go @@ -72,9 +72,9 @@ func (p *PodSpecGenerator) Validate() error { return exclusivePodOptions("NoInfra", "NoManageResolvConf") } } - if p.NetNS.NSMode != "" && p.NetNS.NSMode != Bridge && p.NetNS.NSMode != Default { + if p.NetNS.NSMode != "" && p.NetNS.NSMode != Bridge && p.NetNS.NSMode != Slirp && p.NetNS.NSMode != Default { if len(p.PortMappings) > 0 { - return errors.New("PortMappings can only be used with Bridge mode networking") + 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") diff --git a/pkg/specgen/podspecgen.go b/pkg/specgen/podspecgen.go index 3c32ec365..7d771f5bb 100644 --- a/pkg/specgen/podspecgen.go +++ b/pkg/specgen/podspecgen.go @@ -134,6 +134,9 @@ type PodNetworkConfig struct { // Conflicts with NoInfra=true and NoManageHosts. // Optional. HostAdd []string `json:"hostadd,omitempty"` + // NetworkOptions are additional options for each network + // Optional. + NetworkOptions map[string][]string `json:"network_options,omitempty"` } // PodCgroupConfig contains configuration options about a pod's cgroups. |