Commit 2f9a89c8 authored by Brad Davidson's avatar Brad Davidson Committed by Brad Davidson

Update rootless-containers

parent 2907fb1e
...@@ -26,6 +26,7 @@ replace ( ...@@ -26,6 +26,7 @@ replace (
github.com/juju/errors => github.com/k3s-io/nocode v0.0.0-20200630202308-cb097102c09f github.com/juju/errors => github.com/k3s-io/nocode v0.0.0-20200630202308-cb097102c09f
github.com/kubernetes-sigs/cri-tools => github.com/k3s-io/cri-tools v1.20.0-k3s1 github.com/kubernetes-sigs/cri-tools => github.com/k3s-io/cri-tools v1.20.0-k3s1
github.com/matryer/moq => github.com/rancher/moq v0.0.0-20190404221404-ee5226d43009 github.com/matryer/moq => github.com/rancher/moq v0.0.0-20190404221404-ee5226d43009
github.com/moby/sys/mountinfo => github.com/moby/sys/mountinfo v0.1.3
// LOOK TO scripts/download FOR THE VERSION OF runc THAT WE ARE BUILDING/SHIPPING // LOOK TO scripts/download FOR THE VERSION OF runc THAT WE ARE BUILDING/SHIPPING
github.com/opencontainers/runc => github.com/opencontainers/runc v1.0.0-rc92 github.com/opencontainers/runc => github.com/opencontainers/runc v1.0.0-rc92
github.com/opencontainers/runtime-spec => github.com/opencontainers/runtime-spec v1.0.3-0.20200728170252-4d89ac9fbff6 github.com/opencontainers/runtime-spec => github.com/opencontainers/runtime-spec v1.0.3-0.20200728170252-4d89ac9fbff6
...@@ -87,7 +88,7 @@ require ( ...@@ -87,7 +88,7 @@ require (
github.com/go-sql-driver/mysql v1.4.1 github.com/go-sql-driver/mysql v1.4.1
github.com/google/cadvisor v0.38.8 github.com/google/cadvisor v0.38.8
github.com/google/tcpproxy v0.0.0-20180808230851-dfa16c61dad2 github.com/google/tcpproxy v0.0.0-20180808230851-dfa16c61dad2
github.com/google/uuid v1.2.0 github.com/google/uuid v1.3.0
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.4.2 github.com/gorilla/websocket v1.4.2
github.com/k3s-io/helm-controller v0.10.5 github.com/k3s-io/helm-controller v0.10.5
...@@ -108,10 +109,10 @@ require ( ...@@ -108,10 +109,10 @@ require (
github.com/rancher/wrangler v0.6.2 github.com/rancher/wrangler v0.6.2
github.com/rancher/wrangler-api v0.6.0 github.com/rancher/wrangler-api v0.6.0
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/rootless-containers/rootlesskit v0.10.0 github.com/rootless-containers/rootlesskit v0.14.5
github.com/sirupsen/logrus v1.7.0 github.com/sirupsen/logrus v1.8.1
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.7.0
github.com/tchap/go-patricia v2.3.0+incompatible // indirect github.com/tchap/go-patricia v2.3.0+incompatible // indirect
github.com/urfave/cli v1.22.2 github.com/urfave/cli v1.22.2
go.etcd.io/etcd v0.5.0-alpha.5.0.20201208200253-50621aee4aea go.etcd.io/etcd v0.5.0-alpha.5.0.20201208200253-50621aee4aea
......
...@@ -143,7 +143,7 @@ func createParentOpt(stateDir string) (*parent.Opt, error) { ...@@ -143,7 +143,7 @@ func createParentOpt(stateDir string) (*parent.Opt, error) {
return nil, err return nil, err
} }
debugWriter := &logrusDebugWriter{} debugWriter := &logrusDebugWriter{}
opt.NetworkDriver, err = slirp4netns.NewParentDriver(debugWriter, binary, mtu, ipnet, disableHostLoopback, "", false, false) opt.NetworkDriver, err = slirp4netns.NewParentDriver(debugWriter, binary, mtu, ipnet, "tap0", disableHostLoopback, "", false, false, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
......
language: go language: go
go: go:
- 1.10.x - 1.14.x
- 1.11.x - 1.15.x
script: go test -v -check.vv -race ./... script: go test -v -check.vv -race ./...
sudo: false sudo: false
notifications: notifications:
......
Copyright (c) 2015, Tim Heckman Copyright (c) 2015-2020, Tim Heckman
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
...@@ -11,9 +11,9 @@ modification, are permitted provided that the following conditions are met: ...@@ -11,9 +11,9 @@ modification, are permitted provided that the following conditions are met:
this list of conditions and the following disclaimer in the documentation this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution. and/or other materials provided with the distribution.
* Neither the name of linode-netint nor the names of its * Neither the name of gofrs nor the names of its contributors may be used
contributors may be used to endorse or promote products derived from to endorse or promote products derived from this software without
this software without specific prior written permission. specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
......
# flock # flock
[![TravisCI Build Status](https://img.shields.io/travis/gofrs/flock/master.svg?style=flat)](https://travis-ci.org/gofrs/flock) [![TravisCI Build Status](https://img.shields.io/travis/gofrs/flock/master.svg?style=flat)](https://travis-ci.org/gofrs/flock)
[![GoDoc](https://img.shields.io/badge/godoc-go--flock-blue.svg?style=flat)](https://godoc.org/github.com/gofrs/flock) [![GoDoc](https://img.shields.io/badge/godoc-flock-blue.svg?style=flat)](https://godoc.org/github.com/gofrs/flock)
[![License](https://img.shields.io/badge/license-BSD_3--Clause-brightgreen.svg?style=flat)](https://github.com/gofrs/flock/blob/master/LICENSE) [![License](https://img.shields.io/badge/license-BSD_3--Clause-brightgreen.svg?style=flat)](https://github.com/gofrs/flock/blob/master/LICENSE)
[![Go Report Card](https://goreportcard.com/badge/github.com/gofrs/flock)](https://goreportcard.com/report/github.com/gofrs/flock) [![Go Report Card](https://goreportcard.com/badge/github.com/gofrs/flock)](https://goreportcard.com/report/github.com/gofrs/flock)
......
...@@ -7,7 +7,7 @@ clone_folder: 'c:\gopath\src\github.com\gofrs\flock' ...@@ -7,7 +7,7 @@ clone_folder: 'c:\gopath\src\github.com\gofrs\flock'
environment: environment:
GOPATH: 'c:\gopath' GOPATH: 'c:\gopath'
GOVERSION: '1.11' GOVERSION: '1.15'
init: init:
- git config --global core.autocrlf input - git config --global core.autocrlf input
......
...@@ -19,6 +19,7 @@ package flock ...@@ -19,6 +19,7 @@ package flock
import ( import (
"context" "context"
"os" "os"
"runtime"
"sync" "sync"
"time" "time"
) )
...@@ -116,7 +117,15 @@ func tryCtx(ctx context.Context, fn func() (bool, error), retryDelay time.Durati ...@@ -116,7 +117,15 @@ func tryCtx(ctx context.Context, fn func() (bool, error), retryDelay time.Durati
func (f *Flock) setFh() error { func (f *Flock) setFh() error {
// open a new os.File instance // open a new os.File instance
// create it if it doesn't exist, and open the file read-only. // create it if it doesn't exist, and open the file read-only.
fh, err := os.OpenFile(f.path, os.O_CREATE|os.O_RDONLY, os.FileMode(0600)) flags := os.O_CREATE
if runtime.GOOS == "aix" {
// AIX cannot preform write-lock (ie exclusive) on a
// read-only file.
flags |= os.O_RDWR
} else {
flags |= os.O_RDONLY
}
fh, err := os.OpenFile(f.path, flags, os.FileMode(0600))
if err != nil { if err != nil {
return err return err
} }
...@@ -125,3 +134,11 @@ func (f *Flock) setFh() error { ...@@ -125,3 +134,11 @@ func (f *Flock) setFh() error {
f.fh = fh f.fh = fh
return nil return nil
} }
// ensure the file handle is closed if no lock is held
func (f *Flock) ensureFhState() {
if !f.l && !f.r && f.fh != nil {
f.fh.Close()
f.fh = nil
}
}
// Copyright 2019 Tim Heckman. All rights reserved. Use of this source code is
// governed by the BSD 3-Clause license that can be found in the LICENSE file.
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This code implements the filelock API using POSIX 'fcntl' locks, which attach
// to an (inode, process) pair rather than a file descriptor. To avoid unlocking
// files prematurely when the same file is opened through different descriptors,
// we allow only one read-lock at a time.
//
// This code is adapted from the Go package:
// cmd/go/internal/lockedfile/internal/filelock
//+build aix
package flock
import (
"errors"
"io"
"os"
"sync"
"syscall"
"golang.org/x/sys/unix"
)
type lockType int16
const (
readLock lockType = unix.F_RDLCK
writeLock lockType = unix.F_WRLCK
)
type cmdType int
const (
tryLock cmdType = unix.F_SETLK
waitLock cmdType = unix.F_SETLKW
)
type inode = uint64
type inodeLock struct {
owner *Flock
queue []<-chan *Flock
}
var (
mu sync.Mutex
inodes = map[*Flock]inode{}
locks = map[inode]inodeLock{}
)
// Lock is a blocking call to try and take an exclusive file lock. It will wait
// until it is able to obtain the exclusive file lock. It's recommended that
// TryLock() be used over this function. This function may block the ability to
// query the current Locked() or RLocked() status due to a RW-mutex lock.
//
// If we are already exclusive-locked, this function short-circuits and returns
// immediately assuming it can take the mutex lock.
//
// If the *Flock has a shared lock (RLock), this may transparently replace the
// shared lock with an exclusive lock on some UNIX-like operating systems. Be
// careful when using exclusive locks in conjunction with shared locks
// (RLock()), because calling Unlock() may accidentally release the exclusive
// lock that was once a shared lock.
func (f *Flock) Lock() error {
return f.lock(&f.l, writeLock)
}
// RLock is a blocking call to try and take a shared file lock. It will wait
// until it is able to obtain the shared file lock. It's recommended that
// TryRLock() be used over this function. This function may block the ability to
// query the current Locked() or RLocked() status due to a RW-mutex lock.
//
// If we are already shared-locked, this function short-circuits and returns
// immediately assuming it can take the mutex lock.
func (f *Flock) RLock() error {
return f.lock(&f.r, readLock)
}
func (f *Flock) lock(locked *bool, flag lockType) error {
f.m.Lock()
defer f.m.Unlock()
if *locked {
return nil
}
if f.fh == nil {
if err := f.setFh(); err != nil {
return err
}
defer f.ensureFhState()
}
if _, err := f.doLock(waitLock, flag, true); err != nil {
return err
}
*locked = true
return nil
}
func (f *Flock) doLock(cmd cmdType, lt lockType, blocking bool) (bool, error) {
// POSIX locks apply per inode and process, and the lock for an inode is
// released when *any* descriptor for that inode is closed. So we need to
// synchronize access to each inode internally, and must serialize lock and
// unlock calls that refer to the same inode through different descriptors.
fi, err := f.fh.Stat()
if err != nil {
return false, err
}
ino := inode(fi.Sys().(*syscall.Stat_t).Ino)
mu.Lock()
if i, dup := inodes[f]; dup && i != ino {
mu.Unlock()
return false, &os.PathError{
Path: f.Path(),
Err: errors.New("inode for file changed since last Lock or RLock"),
}
}
inodes[f] = ino
var wait chan *Flock
l := locks[ino]
if l.owner == f {
// This file already owns the lock, but the call may change its lock type.
} else if l.owner == nil {
// No owner: it's ours now.
l.owner = f
} else if !blocking {
// Already owned: cannot take the lock.
mu.Unlock()
return false, nil
} else {
// Already owned: add a channel to wait on.
wait = make(chan *Flock)
l.queue = append(l.queue, wait)
}
locks[ino] = l
mu.Unlock()
if wait != nil {
wait <- f
}
err = setlkw(f.fh.Fd(), cmd, lt)
if err != nil {
f.doUnlock()
if cmd == tryLock && err == unix.EACCES {
return false, nil
}
return false, err
}
return true, nil
}
func (f *Flock) Unlock() error {
f.m.Lock()
defer f.m.Unlock()
// if we aren't locked or if the lockfile instance is nil
// just return a nil error because we are unlocked
if (!f.l && !f.r) || f.fh == nil {
return nil
}
if err := f.doUnlock(); err != nil {
return err
}
f.fh.Close()
f.l = false
f.r = false
f.fh = nil
return nil
}
func (f *Flock) doUnlock() (err error) {
var owner *Flock
mu.Lock()
ino, ok := inodes[f]
if ok {
owner = locks[ino].owner
}
mu.Unlock()
if owner == f {
err = setlkw(f.fh.Fd(), waitLock, unix.F_UNLCK)
}
mu.Lock()
l := locks[ino]
if len(l.queue) == 0 {
// No waiters: remove the map entry.
delete(locks, ino)
} else {
// The first waiter is sending us their file now.
// Receive it and update the queue.
l.owner = <-l.queue[0]
l.queue = l.queue[1:]
locks[ino] = l
}
delete(inodes, f)
mu.Unlock()
return err
}
// TryLock is the preferred function for taking an exclusive file lock. This
// function takes an RW-mutex lock before it tries to lock the file, so there is
// the possibility that this function may block for a short time if another
// goroutine is trying to take any action.
//
// The actual file lock is non-blocking. If we are unable to get the exclusive
// file lock, the function will return false instead of waiting for the lock. If
// we get the lock, we also set the *Flock instance as being exclusive-locked.
func (f *Flock) TryLock() (bool, error) {
return f.try(&f.l, writeLock)
}
// TryRLock is the preferred function for taking a shared file lock. This
// function takes an RW-mutex lock before it tries to lock the file, so there is
// the possibility that this function may block for a short time if another
// goroutine is trying to take any action.
//
// The actual file lock is non-blocking. If we are unable to get the shared file
// lock, the function will return false instead of waiting for the lock. If we
// get the lock, we also set the *Flock instance as being share-locked.
func (f *Flock) TryRLock() (bool, error) {
return f.try(&f.r, readLock)
}
func (f *Flock) try(locked *bool, flag lockType) (bool, error) {
f.m.Lock()
defer f.m.Unlock()
if *locked {
return true, nil
}
if f.fh == nil {
if err := f.setFh(); err != nil {
return false, err
}
defer f.ensureFhState()
}
haslock, err := f.doLock(tryLock, flag, false)
if err != nil {
return false, err
}
*locked = haslock
return haslock, nil
}
// setlkw calls FcntlFlock with cmd for the entire file indicated by fd.
func setlkw(fd uintptr, cmd cmdType, lt lockType) error {
for {
err := unix.FcntlFlock(fd, int(cmd), &unix.Flock_t{
Type: int16(lt),
Whence: io.SeekStart,
Start: 0,
Len: 0, // All bytes.
})
if err != unix.EINTR {
return err
}
}
}
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// Use of this source code is governed by the BSD 3-Clause // Use of this source code is governed by the BSD 3-Clause
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build !windows // +build !aix,!windows
package flock package flock
...@@ -51,6 +51,7 @@ func (f *Flock) lock(locked *bool, flag int) error { ...@@ -51,6 +51,7 @@ func (f *Flock) lock(locked *bool, flag int) error {
if err := f.setFh(); err != nil { if err := f.setFh(); err != nil {
return err return err
} }
defer f.ensureFhState()
} }
if err := syscall.Flock(int(f.fh.Fd()), flag); err != nil { if err := syscall.Flock(int(f.fh.Fd()), flag); err != nil {
...@@ -142,6 +143,7 @@ func (f *Flock) try(locked *bool, flag int) (bool, error) { ...@@ -142,6 +143,7 @@ func (f *Flock) try(locked *bool, flag int) (bool, error) {
if err := f.setFh(); err != nil { if err := f.setFh(); err != nil {
return false, err return false, err
} }
defer f.ensureFhState()
} }
var retried bool var retried bool
......
...@@ -46,6 +46,7 @@ func (f *Flock) lock(locked *bool, flag uint32) error { ...@@ -46,6 +46,7 @@ func (f *Flock) lock(locked *bool, flag uint32) error {
if err := f.setFh(); err != nil { if err := f.setFh(); err != nil {
return err return err
} }
defer f.ensureFhState()
} }
if _, errNo := lockFileEx(syscall.Handle(f.fh.Fd()), flag, 0, 1, 0, &syscall.Overlapped{}); errNo > 0 { if _, errNo := lockFileEx(syscall.Handle(f.fh.Fd()), flag, 0, 1, 0, &syscall.Overlapped{}); errNo > 0 {
...@@ -122,6 +123,7 @@ func (f *Flock) try(locked *bool, flag uint32) (bool, error) { ...@@ -122,6 +123,7 @@ func (f *Flock) try(locked *bool, flag uint32) (bool, error) {
if err := f.setFh(); err != nil { if err := f.setFh(); err != nil {
return false, err return false, err
} }
defer f.ensureFhState()
} }
_, errNo := lockFileEx(syscall.Handle(f.fh.Fd()), flag|winLockfileFailImmediately, 0, 1, 0, &syscall.Overlapped{}) _, errNo := lockFileEx(syscall.Handle(f.fh.Fd()), flag|winLockfileFailImmediately, 0, 1, 0, &syscall.Overlapped{})
......
// Copyright 2021 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"bytes"
"database/sql/driver"
"encoding/json"
"fmt"
)
var jsonNull = []byte("null")
// NullUUID represents a UUID that may be null.
// NullUUID implements the SQL driver.Scanner interface so
// it can be used as a scan destination:
//
// var u uuid.NullUUID
// err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&u)
// ...
// if u.Valid {
// // use u.UUID
// } else {
// // NULL value
// }
//
type NullUUID struct {
UUID UUID
Valid bool // Valid is true if UUID is not NULL
}
// Scan implements the SQL driver.Scanner interface.
func (nu *NullUUID) Scan(value interface{}) error {
if value == nil {
nu.UUID, nu.Valid = Nil, false
return nil
}
err := nu.UUID.Scan(value)
if err != nil {
nu.Valid = false
return err
}
nu.Valid = true
return nil
}
// Value implements the driver Valuer interface.
func (nu NullUUID) Value() (driver.Value, error) {
if !nu.Valid {
return nil, nil
}
// Delegate to UUID Value function
return nu.UUID.Value()
}
// MarshalBinary implements encoding.BinaryMarshaler.
func (nu NullUUID) MarshalBinary() ([]byte, error) {
if nu.Valid {
return nu.UUID[:], nil
}
return []byte(nil), nil
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
func (nu *NullUUID) UnmarshalBinary(data []byte) error {
if len(data) != 16 {
return fmt.Errorf("invalid UUID (got %d bytes)", len(data))
}
copy(nu.UUID[:], data)
nu.Valid = true
return nil
}
// MarshalText implements encoding.TextMarshaler.
func (nu NullUUID) MarshalText() ([]byte, error) {
if nu.Valid {
return nu.UUID.MarshalText()
}
return jsonNull, nil
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (nu *NullUUID) UnmarshalText(data []byte) error {
id, err := ParseBytes(data)
if err != nil {
nu.Valid = false
return err
}
nu.UUID = id
nu.Valid = true
return nil
}
// MarshalJSON implements json.Marshaler.
func (nu NullUUID) MarshalJSON() ([]byte, error) {
if nu.Valid {
return json.Marshal(nu.UUID)
}
return jsonNull, nil
}
// UnmarshalJSON implements json.Unmarshaler.
func (nu *NullUUID) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, jsonNull) {
*nu = NullUUID{}
return nil // valid null UUID
}
err := json.Unmarshal(data, &nu.UUID)
nu.Valid = err == nil
return err
}
...@@ -12,6 +12,7 @@ import ( ...@@ -12,6 +12,7 @@ import (
"fmt" "fmt"
"io" "io"
"strings" "strings"
"sync"
) )
// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC // A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC
...@@ -33,7 +34,15 @@ const ( ...@@ -33,7 +34,15 @@ const (
Future // Reserved for future definition. Future // Reserved for future definition.
) )
var rander = rand.Reader // random function const randPoolSize = 16 * 16
var (
rander = rand.Reader // random function
poolEnabled = false
poolMu sync.Mutex
poolPos = randPoolSize // protected with poolMu
pool [randPoolSize]byte // protected with poolMu
)
type invalidLengthError struct{ len int } type invalidLengthError struct{ len int }
...@@ -41,6 +50,12 @@ func (err invalidLengthError) Error() string { ...@@ -41,6 +50,12 @@ func (err invalidLengthError) Error() string {
return fmt.Sprintf("invalid UUID length: %d", err.len) return fmt.Sprintf("invalid UUID length: %d", err.len)
} }
// IsInvalidLengthError is matcher function for custom error invalidLengthError
func IsInvalidLengthError(err error) bool {
_, ok := err.(invalidLengthError)
return ok
}
// Parse decodes s into a UUID or returns an error. Both the standard UUID // Parse decodes s into a UUID or returns an error. Both the standard UUID
// forms of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and // forms of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded as well as the // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded as well as the
...@@ -249,3 +264,31 @@ func SetRand(r io.Reader) { ...@@ -249,3 +264,31 @@ func SetRand(r io.Reader) {
} }
rander = r rander = r
} }
// EnableRandPool enables internal randomness pool used for Random
// (Version 4) UUID generation. The pool contains random bytes read from
// the random number generator on demand in batches. Enabling the pool
// may improve the UUID generation throughput significantly.
//
// Since the pool is stored on the Go heap, this feature may be a bad fit
// for security sensitive applications.
//
// Both EnableRandPool and DisableRandPool are not thread-safe and should
// only be called when there is no possibility that New or any other
// UUID Version 4 generation function will be called concurrently.
func EnableRandPool() {
poolEnabled = true
}
// DisableRandPool disables the randomness pool if it was previously
// enabled with EnableRandPool.
//
// Both EnableRandPool and DisableRandPool are not thread-safe and should
// only be called when there is no possibility that New or any other
// UUID Version 4 generation function will be called concurrently.
func DisableRandPool() {
poolEnabled = false
defer poolMu.Unlock()
poolMu.Lock()
poolPos = randPoolSize
}
...@@ -27,6 +27,8 @@ func NewString() string { ...@@ -27,6 +27,8 @@ func NewString() string {
// The strength of the UUIDs is based on the strength of the crypto/rand // The strength of the UUIDs is based on the strength of the crypto/rand
// package. // package.
// //
// Uses the randomness pool if it was enabled with EnableRandPool.
//
// A note about uniqueness derived from the UUID Wikipedia entry: // A note about uniqueness derived from the UUID Wikipedia entry:
// //
// Randomly generated UUIDs have 122 random bits. One's annual risk of being // Randomly generated UUIDs have 122 random bits. One's annual risk of being
...@@ -35,7 +37,10 @@ func NewString() string { ...@@ -35,7 +37,10 @@ func NewString() string {
// equivalent to the odds of creating a few tens of trillions of UUIDs in a // equivalent to the odds of creating a few tens of trillions of UUIDs in a
// year and having one duplicate. // year and having one duplicate.
func NewRandom() (UUID, error) { func NewRandom() (UUID, error) {
return NewRandomFromReader(rander) if !poolEnabled {
return NewRandomFromReader(rander)
}
return newRandomFromPool()
} }
// NewRandomFromReader returns a UUID based on bytes read from a given io.Reader. // NewRandomFromReader returns a UUID based on bytes read from a given io.Reader.
...@@ -49,3 +54,23 @@ func NewRandomFromReader(r io.Reader) (UUID, error) { ...@@ -49,3 +54,23 @@ func NewRandomFromReader(r io.Reader) (UUID, error) {
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
return uuid, nil return uuid, nil
} }
func newRandomFromPool() (UUID, error) {
var uuid UUID
poolMu.Lock()
if poolPos == randPoolSize {
_, err := io.ReadFull(rander, pool[:])
if err != nil {
poolMu.Unlock()
return Nil, err
}
poolPos = 0
}
copy(uuid[:], pool[poolPos:(poolPos+16)])
poolPos += 16
poolMu.Unlock()
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
return uuid, nil
}
package api package api
import "net"
const (
// Version of the REST API, not implementation version.
// See openapi.yaml for the definition.
Version = "1.1.1"
)
// ErrorJSON is returned with "application/json" content type and non-2XX status code // ErrorJSON is returned with "application/json" content type and non-2XX status code
type ErrorJSON struct { type ErrorJSON struct {
Message string `json:"message"` Message string `json:"message"`
} }
// Info is the structure returned by `GET /info`
type Info struct {
APIVersion string `json:"apiVersion"` // REST API version
Version string `json:"version"` // Implementation version
StateDir string `json:"stateDir"`
ChildPID int `json:"childPID"`
NetworkDriver *NetworkDriverInfo `json:"networkDriver,omitempty"`
PortDriver *PortDriverInfo `json:"portDriver,omitempty"`
}
// NetworkDriverInfo in Info
type NetworkDriverInfo struct {
Driver string `json:"driver"`
DNS []net.IP `json:"dns,omitempty"`
ChildIP net.IP `json:"childIP,omitempty"` // since API v1.1.1 (RootlessKit v0.14.1)
DynamicChildIP bool `json:"dynamicChildIP,omitempty"` // since API v1.1.1
}
// PortDriverInfo in Info
type PortDriverInfo struct {
Driver string `json:"driver"`
Protos []string `json:"protos"`
DisallowLoopbackChildIP bool `json:"disallowLoopbackChildIP,omitempty"` // since API v1.1.1
}
...@@ -20,6 +20,7 @@ import ( ...@@ -20,6 +20,7 @@ import (
type Client interface { type Client interface {
HTTPClient() *http.Client HTTPClient() *http.Client
PortManager() port.Manager PortManager() port.Manager
Info(context.Context) (*api.Info, error)
} }
// New creates a client. // New creates a client.
...@@ -65,6 +66,29 @@ func (c *client) PortManager() port.Manager { ...@@ -65,6 +66,29 @@ func (c *client) PortManager() port.Manager {
} }
} }
func (c *client) Info(ctx context.Context) (*api.Info, error) {
u := fmt.Sprintf("http://%s/%s/info", c.dummyHost, c.version)
req, err := http.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
resp, err := c.HTTPClient().Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if err := successful(resp); err != nil {
return nil, err
}
var info api.Info
dec := json.NewDecoder(resp.Body)
if err := dec.Decode(&info); err != nil {
return nil, err
}
return &info, nil
}
func readAtMost(r io.Reader, maxBytes int) ([]byte, error) { func readAtMost(r io.Reader, maxBytes int) ([]byte, error) {
lr := &io.LimitedReader{ lr := &io.LimitedReader{
R: r, R: r,
......
# When you made a change to this YAML, please validate with https://editor.swagger.io # When you made a change to this YAML, please validate with https://editor.swagger.io
openapi: 3.0.2 openapi: 3.0.3
info: info:
version: 1.0.0 version: 1.1.1
title: RootlessKit API title: RootlessKit API
servers: servers:
- url: 'http://rootlesskit/v1' - url: 'http://rootlesskit/v1'
description: Local UNIX socket server. The host part of the URL is ignored. description: Local UNIX socket server. The host part of the URL is ignored.
paths: paths:
# /info: API >= 1.1.0
/info:
get:
responses:
'200':
description: Info. Available since API 1.1.0.
content:
application/json:
schema:
$ref: '#/components/schemas/Info'
/ports: /ports:
get: get:
responses: responses:
...@@ -44,16 +54,25 @@ paths: ...@@ -44,16 +54,25 @@ paths:
description: Null response description: Null response
components: components:
schemas: schemas:
Proto:
type: string
description: "protocol for listening. Corresponds to Go's net.Listen. The strings with \"4\" and \"6\" suffixes were introduced in API 1.1.0."
enum:
- tcp
- tcp4
- tcp6
- udp
- udp4
- udp6
- sctp
- sctp4
- sctp6
PortSpec: PortSpec:
required: required:
- proto - proto
properties: properties:
proto: proto:
type: string $ref: '#/components/schemas/Proto'
enum:
- tcp
- udp
- sctp
parentIP: parentIP:
type: string type: string
parentPort: parentPort:
...@@ -61,6 +80,8 @@ components: ...@@ -61,6 +80,8 @@ components:
format: int32 format: int32
minimum: 1 minimum: 1
maximum: 65535 maximum: 65535
childIP:
type: string
# future version may support requests with parentPort<=0 for automatic port assignment # future version may support requests with parentPort<=0 for automatic port assignment
childPort: childPort:
type: integer type: integer
...@@ -80,3 +101,71 @@ components: ...@@ -80,3 +101,71 @@ components:
type: array type: array
items: items:
$ref: '#/components/schemas/PortStatus' $ref: '#/components/schemas/PortStatus'
# Info: API >= 1.1.0
Info:
required:
- apiVersion
- version
- stateDir
- childPID
properties:
apiVersion:
type: string
description: "API version, without \"v\" prefix"
example: "1.1.0"
version:
type: string
description: "Implementation version, without \"v\" prefix"
example: "0.42.0-beta.1+dev"
stateDir:
type: string
description: "state dir"
example: "/run/user/1000/rootlesskit"
childPID:
type: integer
description: "child PID"
example: 10042
networkDriver:
$ref: '#/components/schemas/NetworkDriverInfo'
portDriver:
$ref: '#/components/schemas/PortDriverInfo'
NetworkDriverInfo:
required:
- driver
properties:
driver:
type: string
description: "network driver. Empty when --net=host."
example: "slirp4netns"
# TODO: return TAP info
dns:
type: array
description: "DNS addresses"
items:
type: string
example: ["10.0.2.3"]
childIP:
type: string
description: "Child IP (v4)"
example: "10.0.2.100"
dynamicChildIP:
type: boolean
description: "Child IP may change"
PortDriverInfo:
required:
- driver
- supportedProtos
properties:
driver:
type: string
description: "port driver"
example: "builtin"
protos:
type: array
description: "The supported protocol strings for listening ports"
example: ["tcp","udp"]
items:
$ref: '#/components/schemas/Proto'
disallowLoopbackChildIP:
type: boolean
description: "If this field is set to true, loopback IP such as 127.0.0.1 cannot be specified as a child IP"
...@@ -11,12 +11,28 @@ import ( ...@@ -11,12 +11,28 @@ import (
"github.com/rootless-containers/rootlesskit/pkg/api" "github.com/rootless-containers/rootlesskit/pkg/api"
"github.com/rootless-containers/rootlesskit/pkg/port" "github.com/rootless-containers/rootlesskit/pkg/port"
"github.com/rootless-containers/rootlesskit/pkg/version"
) )
// NetworkDriver is implemented by network.ParentDriver
type NetworkDriver interface {
Info(context.Context) (*api.NetworkDriverInfo, error)
}
// PortDriver is implemented by port.ParentDriver
type PortDriver interface {
Info(context.Context) (*api.PortDriverInfo, error)
port.Manager
}
type Backend struct { type Backend struct {
StateDir string
ChildPID int
// NetworkDriver can be nil
NetworkDriver NetworkDriver
// PortDriver MUST be thread-safe. // PortDriver MUST be thread-safe.
// PortDriver can be nil // PortDriver can be nil
PortDriver port.ParentDriver PortDriver PortDriver
} }
func (b *Backend) onError(w http.ResponseWriter, r *http.Request, err error, ec int) { func (b *Backend) onError(w http.ResponseWriter, r *http.Request, err error, ec int) {
...@@ -104,9 +120,43 @@ func (b *Backend) DeletePort(w http.ResponseWriter, r *http.Request) { ...@@ -104,9 +120,43 @@ func (b *Backend) DeletePort(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
func (b *Backend) GetInfo(w http.ResponseWriter, r *http.Request) {
info := &api.Info{
APIVersion: api.Version,
Version: version.Version,
StateDir: b.StateDir,
ChildPID: b.ChildPID,
}
if b.NetworkDriver != nil {
ndInfo, err := b.NetworkDriver.Info(context.Background())
if err != nil {
b.onError(w, r, err, http.StatusInternalServerError)
return
}
info.NetworkDriver = ndInfo
}
if b.PortDriver != nil {
pdInfo, err := b.PortDriver.Info(context.Background())
if err != nil {
b.onError(w, r, err, http.StatusInternalServerError)
return
}
info.PortDriver = pdInfo
}
m, err := json.Marshal(info)
if err != nil {
b.onError(w, r, err, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(m)
}
func AddRoutes(r *mux.Router, b *Backend) { func AddRoutes(r *mux.Router, b *Backend) {
v1 := r.PathPrefix("/v1").Subrouter() v1 := r.PathPrefix("/v1").Subrouter()
v1.Path("/ports").Methods("GET").HandlerFunc(b.GetPorts) v1.Path("/ports").Methods("GET").HandlerFunc(b.GetPorts)
v1.Path("/ports").Methods("POST").HandlerFunc(b.PostPort) v1.Path("/ports").Methods("POST").HandlerFunc(b.PostPort)
v1.Path("/ports/{id}").Methods("DELETE").HandlerFunc(b.DeletePort) v1.Path("/ports/{id}").Methods("DELETE").HandlerFunc(b.DeletePort)
v1.Path("/info").Methods("GET").HandlerFunc(b.GetInfo)
} }
...@@ -2,6 +2,7 @@ package child ...@@ -2,6 +2,7 @@ package child
import ( import (
"context" "context"
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
...@@ -50,15 +51,31 @@ func createCmd(targetCmd []string) (*exec.Cmd, error) { ...@@ -50,15 +51,31 @@ func createCmd(targetCmd []string) (*exec.Cmd, error) {
// mountSysfs is needed for mounting /sys/class/net // mountSysfs is needed for mounting /sys/class/net
// when netns is unshared. // when netns is unshared.
func mountSysfs() error { func mountSysfs(hostNetwork, evacuateCgroup2 bool) error {
const cgroupDir = "/sys/fs/cgroup"
if hostNetwork {
if evacuateCgroup2 {
// We need to mount tmpfs before cgroup2 to avoid EBUSY
if err := unix.Mount("none", cgroupDir, "tmpfs", 0, ""); err != nil {
return errors.Wrapf(err, "failed to mount tmpfs on %s", cgroupDir)
}
if err := unix.Mount("none", cgroupDir, "cgroup2", 0, ""); err != nil {
return errors.Wrapf(err, "failed to mount cgroup2 on %s", cgroupDir)
}
}
// NOP
return nil
}
tmp, err := ioutil.TempDir("/tmp", "rksys") tmp, err := ioutil.TempDir("/tmp", "rksys")
if err != nil { if err != nil {
return errors.Wrap(err, "creating a directory under /tmp") return errors.Wrap(err, "creating a directory under /tmp")
} }
defer os.RemoveAll(tmp) defer os.RemoveAll(tmp)
cgroupDir := "/sys/fs/cgroup" if !evacuateCgroup2 {
if err := unix.Mount(cgroupDir, tmp, "", uintptr(unix.MS_BIND|unix.MS_REC), ""); err != nil { if err := unix.Mount(cgroupDir, tmp, "", uintptr(unix.MS_BIND|unix.MS_REC), ""); err != nil {
return errors.Wrapf(err, "failed to create bind mount on %s", cgroupDir) return errors.Wrapf(err, "failed to create bind mount on %s", cgroupDir)
}
} }
if err := unix.Mount("none", "/sys", "sysfs", 0, ""); err != nil { if err := unix.Mount("none", "/sys", "sysfs", 0, ""); err != nil {
...@@ -72,8 +89,14 @@ func mountSysfs() error { ...@@ -72,8 +89,14 @@ func mountSysfs() error {
logrus.Warnf("failed to mount sysfs: %v", err) logrus.Warnf("failed to mount sysfs: %v", err)
} }
} }
if err := unix.Mount(tmp, cgroupDir, "", uintptr(unix.MS_MOVE), ""); err != nil { if evacuateCgroup2 {
return errors.Wrapf(err, "failed to move mount point from %s to %s", tmp, cgroupDir) if err := unix.Mount("none", cgroupDir, "cgroup2", 0, ""); err != nil {
return errors.Wrapf(err, "failed to mount cgroup2 on %s", cgroupDir)
}
} else {
if err := unix.Mount(tmp, cgroupDir, "", uintptr(unix.MS_MOVE), ""); err != nil {
return errors.Wrapf(err, "failed to move mount point from %s to %s", tmp, cgroupDir)
}
} }
return nil return nil
} }
...@@ -134,10 +157,6 @@ func setupNet(msg common.Message, etcWasCopied bool, driver network.ChildDriver) ...@@ -134,10 +157,6 @@ func setupNet(msg common.Message, etcWasCopied bool, driver network.ChildDriver)
if driver == nil { if driver == nil {
return nil return nil
} }
// for /sys/class/net
if err := mountSysfs(); err != nil {
return err
}
if err := activateLoopback(); err != nil { if err := activateLoopback(); err != nil {
return err return err
} }
...@@ -171,15 +190,16 @@ func setupNet(msg common.Message, etcWasCopied bool, driver network.ChildDriver) ...@@ -171,15 +190,16 @@ func setupNet(msg common.Message, etcWasCopied bool, driver network.ChildDriver)
} }
type Opt struct { type Opt struct {
PipeFDEnvKey string // needs to be set PipeFDEnvKey string // needs to be set
TargetCmd []string // needs to be set TargetCmd []string // needs to be set
NetworkDriver network.ChildDriver // nil for HostNetwork NetworkDriver network.ChildDriver // nil for HostNetwork
CopyUpDriver copyup.ChildDriver // cannot be nil if len(CopyUpDirs) != 0 CopyUpDriver copyup.ChildDriver // cannot be nil if len(CopyUpDirs) != 0
CopyUpDirs []string CopyUpDirs []string
PortDriver port.ChildDriver PortDriver port.ChildDriver
MountProcfs bool // needs to be set if (and only if) parent.Opt.CreatePIDNS is set MountProcfs bool // needs to be set if (and only if) parent.Opt.CreatePIDNS is set
Propagation string // mount propagation type Propagation string // mount propagation type
Reaper bool Reaper bool
EvacuateCgroup2 bool // needs to correspond to parent.Opt.EvacuateCgroup2 is set
} }
func Child(opt Opt) error { func Child(opt Opt) error {
...@@ -234,6 +254,9 @@ func Child(opt Opt) error { ...@@ -234,6 +254,9 @@ func Child(opt Opt) error {
if err != nil { if err != nil {
return err return err
} }
if err := mountSysfs(opt.NetworkDriver == nil, opt.EvacuateCgroup2); err != nil {
return err
}
if err := setupNet(msg, etcWasCopied, opt.NetworkDriver); err != nil { if err := setupNet(msg, etcWasCopied, opt.NetworkDriver); err != nil {
return err return err
} }
...@@ -288,6 +311,7 @@ func setMountPropagation(propagation string) error { ...@@ -288,6 +311,7 @@ func setMountPropagation(propagation string) error {
func runAndReap(cmd *exec.Cmd) error { func runAndReap(cmd *exec.Cmd) error {
c := make(chan os.Signal, 32) c := make(chan os.Signal, 32)
signal.Notify(c, syscall.SIGCHLD) signal.Notify(c, syscall.SIGCHLD)
cmd.SysProcAttr.Setsid = true
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
return err return err
} }
...@@ -297,17 +321,54 @@ func runAndReap(cmd *exec.Cmd) error { ...@@ -297,17 +321,54 @@ func runAndReap(cmd *exec.Cmd) error {
result := make(chan error) result := make(chan error)
go func() { go func() {
defer close(result) defer close(result)
for range c { for cEntry := range c {
for { logrus.Debugf("reaper: got signal %q", cEntry)
if pid, err := syscall.Wait4(-1, nil, syscall.WNOHANG, nil); err != nil || pid <= 0 { if wsPtr := reap(cmd.Process.Pid); wsPtr != nil {
break ws := *wsPtr
} else { if ws.Exited() && ws.ExitStatus() == 0 {
if pid == cmd.Process.Pid { result <- nil
result <- cmd.Wait() continue
}
} }
var resultErr common.ErrorWithSys = &reaperErr{
ws: ws,
}
result <- resultErr
} }
} }
}() }()
return <-result return <-result
} }
func reap(myPid int) *syscall.WaitStatus {
var res *syscall.WaitStatus
for {
var ws syscall.WaitStatus
pid, err := syscall.Wait4(-1, &ws, syscall.WNOHANG, nil)
logrus.Debugf("reaper: got ws=%+v, pid=%d, err=%+v", ws, pid, err)
if err != nil || pid <= 0 {
break
}
if pid == myPid {
res = &ws
}
}
return res
}
type reaperErr struct {
ws syscall.WaitStatus
}
func (e *reaperErr) Sys() interface{} {
return e.ws
}
func (e *reaperErr) Error() string {
if e.ws.Exited() {
return fmt.Sprintf("exit status %d", e.ws.ExitStatus())
}
if e.ws.Signaled() {
return fmt.Sprintf("signal: %s", e.ws.Signal())
}
return fmt.Sprintf("exited with WAITSTATUS=0x%08x", e.ws)
}
...@@ -9,12 +9,18 @@ import ( ...@@ -9,12 +9,18 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// ErrorWithSys is implemented by *exec.ExitError and *child.reaperErr
type ErrorWithSys interface {
error
Sys() interface{}
}
func GetExecExitStatus(err error) (int, bool) { func GetExecExitStatus(err error) (int, bool) {
err = errors.Cause(err) err = errors.Cause(err)
if err == nil { if err == nil {
return 0, false return 0, false
} }
exitErr, ok := err.(*exec.ExitError) exitErr, ok := err.(ErrorWithSys)
if !ok { if !ok {
return 0, false return 0, false
} }
......
package network package network
import ( import (
"context"
"github.com/rootless-containers/rootlesskit/pkg/api"
"github.com/rootless-containers/rootlesskit/pkg/common" "github.com/rootless-containers/rootlesskit/pkg/common"
) )
// ParentDriver is called from the parent namespace // ParentDriver is called from the parent namespace
type ParentDriver interface { type ParentDriver interface {
Info(ctx context.Context) (*api.NetworkDriverInfo, error)
// MTU returns MTU // MTU returns MTU
MTU() int MTU() int
// ConfigureNetwork sets up Slirp, updates msg, and returns destructor function. // ConfigureNetwork sets up Slirp, updates msg, and returns destructor function.
......
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"os/exec" "os/exec"
"strconv" "strconv"
"strings" "strings"
"sync"
"syscall" "syscall"
"time" "time"
...@@ -15,6 +16,7 @@ import ( ...@@ -15,6 +16,7 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"github.com/rootless-containers/rootlesskit/pkg/api"
"github.com/rootless-containers/rootlesskit/pkg/common" "github.com/rootless-containers/rootlesskit/pkg/common"
"github.com/rootless-containers/rootlesskit/pkg/network" "github.com/rootless-containers/rootlesskit/pkg/network"
"github.com/rootless-containers/rootlesskit/pkg/network/iputils" "github.com/rootless-containers/rootlesskit/pkg/network/iputils"
...@@ -22,6 +24,8 @@ import ( ...@@ -22,6 +24,8 @@ import (
) )
type Features struct { type Features struct {
// SupportsEnableIPv6 --enable-ipv6 (v0.2.0)
SupportsEnableIPv6 bool
// SupportsCIDR --cidr (v0.3.0) // SupportsCIDR --cidr (v0.3.0)
SupportsCIDR bool SupportsCIDR bool
// SupportsDisableHostLoopback --disable-host-loopback (v0.3.0) // SupportsDisableHostLoopback --disable-host-loopback (v0.3.0)
...@@ -63,6 +67,7 @@ func DetectFeatures(binary string) (*Features, error) { ...@@ -63,6 +67,7 @@ func DetectFeatures(binary string) (*Features, error) {
kernelSupportsEnableSeccomp = unix.Prctl(unix.PR_SET_SECCOMP, unix.SECCOMP_MODE_FILTER, 0, 0, 0) != unix.EINVAL kernelSupportsEnableSeccomp = unix.Prctl(unix.PR_SET_SECCOMP, unix.SECCOMP_MODE_FILTER, 0, 0, 0) != unix.EINVAL
} }
f := Features{ f := Features{
SupportsEnableIPv6: strings.Contains(s, "--enable-ipv6"),
SupportsCIDR: strings.Contains(s, "--cidr"), SupportsCIDR: strings.Contains(s, "--cidr"),
SupportsDisableHostLoopback: strings.Contains(s, "--disable-host-loopback"), SupportsDisableHostLoopback: strings.Contains(s, "--disable-host-loopback"),
SupportsAPISocket: strings.Contains(s, "--api-socket"), SupportsAPISocket: strings.Contains(s, "--api-socket"),
...@@ -75,7 +80,8 @@ func DetectFeatures(binary string) (*Features, error) { ...@@ -75,7 +80,8 @@ func DetectFeatures(binary string) (*Features, error) {
// NewParentDriver instantiates new parent driver. // NewParentDriver instantiates new parent driver.
// Requires slirp4netns v0.4.0 or later. // Requires slirp4netns v0.4.0 or later.
func NewParentDriver(logWriter io.Writer, binary string, mtu int, ipnet *net.IPNet, disableHostLoopback bool, apiSocketPath string, enableSandbox, enableSeccomp bool) (network.ParentDriver, error) { func NewParentDriver(logWriter io.Writer, binary string, mtu int, ipnet *net.IPNet, ifname string, disableHostLoopback bool, apiSocketPath string,
enableSandbox, enableSeccomp, enableIPv6 bool) (network.ParentDriver, error) {
if binary == "" { if binary == "" {
return nil, errors.New("got empty slirp4netns binary") return nil, errors.New("got empty slirp4netns binary")
} }
...@@ -85,10 +91,18 @@ func NewParentDriver(logWriter io.Writer, binary string, mtu int, ipnet *net.IPN ...@@ -85,10 +91,18 @@ func NewParentDriver(logWriter io.Writer, binary string, mtu int, ipnet *net.IPN
if mtu == 0 { if mtu == 0 {
mtu = 65520 mtu = 65520
} }
if ifname == "" {
ifname = "tap0"
}
features, err := DetectFeatures(binary) features, err := DetectFeatures(binary)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if enableIPv6 && !features.SupportsEnableIPv6 {
return nil, errors.New("this version of slirp4netns does not support --enable-sandbox")
}
if ipnet != nil && !features.SupportsCIDR { if ipnet != nil && !features.SupportsCIDR {
return nil, errors.New("this version of slirp4netns does not support --cidr") return nil, errors.New("this version of slirp4netns does not support --cidr")
} }
...@@ -117,6 +131,8 @@ func NewParentDriver(logWriter io.Writer, binary string, mtu int, ipnet *net.IPN ...@@ -117,6 +131,8 @@ func NewParentDriver(logWriter io.Writer, binary string, mtu int, ipnet *net.IPN
apiSocketPath: apiSocketPath, apiSocketPath: apiSocketPath,
enableSandbox: enableSandbox, enableSandbox: enableSandbox,
enableSeccomp: enableSeccomp, enableSeccomp: enableSeccomp,
enableIPv6: enableIPv6,
ifname: ifname,
}, nil }, nil
} }
...@@ -129,6 +145,25 @@ type parentDriver struct { ...@@ -129,6 +145,25 @@ type parentDriver struct {
apiSocketPath string apiSocketPath string
enableSandbox bool enableSandbox bool
enableSeccomp bool enableSeccomp bool
enableIPv6 bool
ifname string
infoMu sync.RWMutex
info func() *api.NetworkDriverInfo
}
const DriverName = "slirp4netns"
func (d *parentDriver) Info(ctx context.Context) (*api.NetworkDriverInfo, error) {
d.infoMu.RLock()
infoFn := d.info
d.infoMu.RUnlock()
if infoFn == nil {
return &api.NetworkDriverInfo{
Driver: DriverName,
}, nil
}
return infoFn(), nil
} }
func (d *parentDriver) MTU() int { func (d *parentDriver) MTU() int {
...@@ -136,7 +171,7 @@ func (d *parentDriver) MTU() int { ...@@ -136,7 +171,7 @@ func (d *parentDriver) MTU() int {
} }
func (d *parentDriver) ConfigureNetwork(childPID int, stateDir string) (*common.NetworkMessage, func() error, error) { func (d *parentDriver) ConfigureNetwork(childPID int, stateDir string) (*common.NetworkMessage, func() error, error) {
tap := "tap0" tap := d.ifname
var cleanups []func() error var cleanups []func() error
if err := parentutils.PrepareTap(childPID, tap); err != nil { if err := parentutils.PrepareTap(childPID, tap); err != nil {
return nil, common.Seq(cleanups), errors.Wrapf(err, "setting up tap %s", tap) return nil, common.Seq(cleanups), errors.Wrapf(err, "setting up tap %s", tap)
...@@ -165,6 +200,9 @@ func (d *parentDriver) ConfigureNetwork(childPID int, stateDir string) (*common. ...@@ -165,6 +200,9 @@ func (d *parentDriver) ConfigureNetwork(childPID int, stateDir string) (*common.
if d.enableSeccomp { if d.enableSeccomp {
opts = append(opts, "--enable-seccomp") opts = append(opts, "--enable-seccomp")
} }
if d.enableIPv6 {
opts = append(opts, "--enable-ipv6")
}
cmd := exec.CommandContext(ctx, d.binary, append(opts, []string{strconv.Itoa(childPID), tap}...)...) cmd := exec.CommandContext(ctx, d.binary, append(opts, []string{strconv.Itoa(childPID), tap}...)...)
// FIXME: Stdout doen't seem captured // FIXME: Stdout doen't seem captured
cmd.Stdout = d.logWriter cmd.Stdout = d.logWriter
...@@ -215,6 +253,17 @@ func (d *parentDriver) ConfigureNetwork(childPID int, stateDir string) (*common. ...@@ -215,6 +253,17 @@ func (d *parentDriver) ConfigureNetwork(childPID int, stateDir string) (*common.
netmsg.Gateway = "10.0.2.2" netmsg.Gateway = "10.0.2.2"
netmsg.DNS = "10.0.2.3" netmsg.DNS = "10.0.2.3"
} }
d.infoMu.Lock()
d.info = func() *api.NetworkDriverInfo {
return &api.NetworkDriverInfo{
Driver: DriverName,
DNS: []net.IP{net.ParseIP(netmsg.DNS)},
ChildIP: net.ParseIP(netmsg.IP),
DynamicChildIP: false,
}
}
d.infoMu.Unlock()
return &netmsg, common.Seq(cleanups), nil return &netmsg, common.Seq(cleanups), nil
} }
......
package cgrouputil
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/moby/sys/mountinfo"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// EvacuateCgroup2 evacuates cgroup2. Must be called in the parent PID namespace.
//
// When the current process belongs to "/foo" group (visible under "/sys/fs/cgroup/foo") and evac is like "bar",
// - All processes in the "/foo" group are moved to "/foo/bar" group, by writing PIDs into "/sys/fs/cgroup/foo/bar/cgroup.procs"
// - As many controllers as possible are enabled for "/foo/*" groups, by writing "/sys/fs/cgroup/foo/cgroup.subtree_control"
//
// Returns nil when cgroup2 is not enabled.
// Ported from https://github.com/rootless-containers/usernetes/commit/46ad812db7489914897ff8b1774f2fab0efda62b
func EvacuateCgroup2(evac string) error {
if evac == "" {
return errors.New("got empty evacuation group name")
}
if strings.Contains(evac, "/") {
return errors.Errorf("unexpected evacuation group name %q: must not contain \"/\"", evac)
}
mountpoint := findCgroup2Mountpoint()
if mountpoint == "" {
logrus.Warn("cgroup2 is not mounted. cgroup2 evacuation is discarded.")
return nil
}
oldGroup := getCgroup2(os.Getpid())
if mountpoint == "" {
logrus.Warn("process is not running with cgroup2. cgroup2 evacuation is discarded.")
return nil
}
newGroup := filepath.Join(oldGroup, evac)
oldPath := filepath.Join(mountpoint, oldGroup)
newPath := filepath.Join(mountpoint, newGroup)
if err := os.MkdirAll(newPath, 0755); err != nil {
return err
}
// evacuate existing procs from oldGroup to newGroup, so that we can enable all controllers including threaded ones
cgroupProcsBytes, err := ioutil.ReadFile(filepath.Join(oldPath, "cgroup.procs"))
if err != nil {
return err
}
for _, pidStr := range strings.Split(string(cgroupProcsBytes), "\n") {
if pidStr == "" || pidStr == "0" {
continue
}
if err := ioutil.WriteFile(filepath.Join(newPath, "cgroup.procs"), []byte(pidStr), 0644); err != nil {
logrus.WithError(err).Warnf("failed to move process %s to cgroup %q", pidStr, newGroup)
}
}
// enable controllers for all subgroups under the oldGroup
controllerBytes, err := ioutil.ReadFile(filepath.Join(oldPath, "cgroup.controllers"))
if err != nil {
return err
}
for _, controller := range strings.Fields(string(controllerBytes)) {
logrus.Debugf("enabling controller %q", controller)
if err := ioutil.WriteFile(filepath.Join(oldPath, "cgroup.subtree_control"), []byte("+"+controller), 0644); err != nil {
logrus.WithError(err).Warnf("failed to enable controller %q", controller)
}
}
return nil
}
func findCgroup2Mountpoint() string {
f := mountinfoFSTypeFilter("cgroup2")
mounts, err := mountinfo.GetMounts(f)
if err != nil {
logrus.WithError(err).Warn("failed to find mountpoint for cgroup2")
return ""
}
if len(mounts) == 0 {
return ""
}
if len(mounts) != 1 {
logrus.Warnf("expected single mountpoint for cgroup2, got %d", len(mounts))
}
return mounts[0].Mountpoint
}
func getCgroup2(pid int) string {
p := fmt.Sprintf("/proc/%d/cgroup", pid)
b, err := ioutil.ReadFile(p)
if err != nil {
logrus.WithError(err).Warnf("failed to read %q", p)
return ""
}
return getCgroup2FromProcPidCgroup(b)
}
func getCgroup2FromProcPidCgroup(b []byte) string {
for _, l := range strings.Split(string(b), "\n") {
if strings.HasPrefix(l, "0::") {
return strings.TrimPrefix(l, "0::")
}
}
return ""
}
package cgrouputil
import (
"fmt"
"reflect"
"strings"
"github.com/moby/sys/mountinfo"
)
// mountinfoFSType returns m.Fstype on mountinfo v0.1.3,
// returns m.FSType on mountinfo v0.4.0.
func mountinfoFSType(m *mountinfo.Info) (string, bool) {
elem := reflect.ValueOf(m).Elem()
for i := 0; i < elem.NumField(); i++ {
typeField := elem.Type().Field(i)
name := typeField.Name
typ := typeField.Type.String()
if strings.ToLower(name) == "fstype" && typ == "string" {
value := elem.Field(i).String()
return value, true
}
}
return "", false
}
// mountinfoFSTypeFilter is reimplementation of mountinfo.FSTypeFilter.
// Temporary solution for supporting both moby/sys/mountinfo@v0.1.3 and @v0.4.0 .
// Will be removed after downstream projects stop using @v0.1.3 .
func mountinfoFSTypeFilter(fstype ...string) mountinfo.FilterFunc {
return func(m *mountinfo.Info) (bool, bool) {
mFSType, ok := mountinfoFSType(m)
if !ok {
panic(fmt.Errorf("failed to get Fstype (FSType) of %+v", m))
}
for _, t := range fstype {
if mFSType == t {
return false, false // don't skeep, keep going
}
}
return true, false // skip, keep going
}
}
...@@ -23,6 +23,7 @@ import ( ...@@ -23,6 +23,7 @@ import (
"github.com/rootless-containers/rootlesskit/pkg/common" "github.com/rootless-containers/rootlesskit/pkg/common"
"github.com/rootless-containers/rootlesskit/pkg/msgutil" "github.com/rootless-containers/rootlesskit/pkg/msgutil"
"github.com/rootless-containers/rootlesskit/pkg/network" "github.com/rootless-containers/rootlesskit/pkg/network"
"github.com/rootless-containers/rootlesskit/pkg/parent/cgrouputil"
"github.com/rootless-containers/rootlesskit/pkg/parent/idtools" "github.com/rootless-containers/rootlesskit/pkg/parent/idtools"
"github.com/rootless-containers/rootlesskit/pkg/port" "github.com/rootless-containers/rootlesskit/pkg/port"
"github.com/rootless-containers/rootlesskit/pkg/sigproxy" "github.com/rootless-containers/rootlesskit/pkg/sigproxy"
...@@ -43,6 +44,7 @@ type Opt struct { ...@@ -43,6 +44,7 @@ type Opt struct {
ParentEUIDEnvKey string // optional env key to propagate geteuid() value ParentEUIDEnvKey string // optional env key to propagate geteuid() value
ParentEGIDEnvKey string // optional env key to propagate getegid() value ParentEGIDEnvKey string // optional env key to propagate getegid() value
Propagation string Propagation string
EvacuateCgroup2 string // e.g. "rootlesskit_evacuation"
} }
// Documented state files. Undocumented ones are subject to change. // Documented state files. Undocumented ones are subject to change.
...@@ -77,29 +79,59 @@ func checkPreflight(opt Opt) error { ...@@ -77,29 +79,59 @@ func checkPreflight(opt Opt) error {
return nil return nil
} }
func Parent(opt Opt) error { // createCleanupLock uses LOCK_SH for preventing automatic cleanup of
if err := checkPreflight(opt); err != nil { // "/tmp/<Our State Dir>" caused by by systemd.
//
// This LOCK_SH lock is different from our lock file in the state dir.
// We could unify the lock file into LOCK_SH, but we are still keeping
// the lock file for a historical reason.
//
// See:
// - https://github.com/rootless-containers/rootlesskit/issues/185
// - https://github.com/rootless-containers/rootlesskit/pull/188
func createCleanupLock(sDir string) error {
//lock state dir when using /tmp/ path
stateDir, err := os.Open(sDir)
if err != nil {
return err return err
} }
lockPath := filepath.Join(opt.StateDir, StateFileLock) err = unix.Flock(int(stateDir.Fd()), unix.LOCK_SH)
lock := flock.NewFlock(lockPath) if err != nil {
logrus.Warnf("Failed to lock the state dir %s", sDir)
}
return nil
}
// LockStateDir creates and locks "lock" file in the state dir.
func LockStateDir(stateDir string) (*flock.Flock, error) {
lockPath := filepath.Join(stateDir, StateFileLock)
lock := flock.New(lockPath)
locked, err := lock.TryLock() locked, err := lock.TryLock()
if err != nil { if err != nil {
return errors.Wrapf(err, "failed to lock %s", lockPath) return nil, errors.Wrapf(err, "failed to lock %s", lockPath)
} }
if !locked { if !locked {
return errors.Errorf("failed to lock %s, another RootlessKit is running with the same state directory?", lockPath) return nil, errors.Errorf("failed to lock %s, another RootlessKit is running with the same state directory?", lockPath)
}
return lock, nil
}
func Parent(opt Opt) error {
if err := checkPreflight(opt); err != nil {
return err
}
err := createCleanupLock(opt.StateDir)
if err != nil {
return err
}
lock, err := LockStateDir(opt.StateDir)
if err != nil {
return err
} }
defer os.RemoveAll(opt.StateDir) defer os.RemoveAll(opt.StateDir)
defer lock.Unlock() defer lock.Unlock()
// when the previous execution crashed, the state dir may not be removed successfully.
// explicitly remove everything in the state dir except the lock file here.
for _, f := range []string{StateFileChildPID} {
p := filepath.Join(opt.StateDir, f)
if err := os.RemoveAll(p); err != nil {
return errors.Wrapf(err, "failed to remove %s", p)
}
}
pipeR, pipeW, err := os.Pipe() pipeR, pipeW, err := os.Pipe()
if err != nil { if err != nil {
...@@ -149,6 +181,12 @@ func Parent(opt Opt) error { ...@@ -149,6 +181,12 @@ func Parent(opt Opt) error {
sigc := sigproxy.ForwardAllSignals(context.TODO(), cmd.Process.Pid) sigc := sigproxy.ForwardAllSignals(context.TODO(), cmd.Process.Pid)
defer signal.StopCatch(sigc) defer signal.StopCatch(sigc)
if opt.EvacuateCgroup2 != "" {
if err := cgrouputil.EvacuateCgroup2(opt.EvacuateCgroup2); err != nil {
return err
}
}
// send message 0 // send message 0
msg := common.Message{ msg := common.Message{
Stage: 0, Stage: 0,
...@@ -223,7 +261,12 @@ func Parent(opt Opt) error { ...@@ -223,7 +261,12 @@ func Parent(opt Opt) error {
} }
// listens the API // listens the API
apiSockPath := filepath.Join(opt.StateDir, StateFileAPISock) apiSockPath := filepath.Join(opt.StateDir, StateFileAPISock)
apiCloser, err := listenServeAPI(apiSockPath, &router.Backend{PortDriver: opt.PortDriver}) apiCloser, err := listenServeAPI(apiSockPath, &router.Backend{
StateDir: opt.StateDir,
ChildPID: cmd.Process.Pid,
NetworkDriver: opt.NetworkDriver,
PortDriver: opt.PortDriver,
})
if err != nil { if err != nil {
return err return err
} }
...@@ -330,3 +373,32 @@ func listenServeAPI(socketPath string, backend *router.Backend) (apiCloser, erro ...@@ -330,3 +373,32 @@ func listenServeAPI(socketPath string, backend *router.Backend) (apiCloser, erro
go srv.Serve(l) go srv.Serve(l)
return srv, nil return srv, nil
} }
// InitStateDir removes everything in the state dir except the lock file.
// This is needed because when the previous execution crashed, the state dir may not be removed successfully.
//
// InitStateDir must be called before calling parent functions.
func InitStateDir(stateDir string) error {
if err := os.MkdirAll(stateDir, 0755); err != nil {
return err
}
lk, err := LockStateDir(stateDir)
if err != nil {
return err
}
defer lk.Unlock()
stateDirStuffs, err := ioutil.ReadDir(stateDir)
if err != nil {
return err
}
for _, f := range stateDirStuffs {
if f.Name() == StateFileLock {
continue
}
p := filepath.Join(stateDir, f.Name())
if err := os.RemoveAll(p); err != nil {
return errors.Wrapf(err, "failed to remove %s", p)
}
}
return nil
}
package child package child
import ( import (
"fmt"
"io" "io"
"net" "net"
"os" "os"
"strconv"
"strings"
"github.com/pkg/errors" "github.com/pkg/errors"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
...@@ -101,12 +102,28 @@ func (d *childDriver) handleConnectInit(c *net.UnixConn, req *msg.Request) error ...@@ -101,12 +102,28 @@ func (d *childDriver) handleConnectInit(c *net.UnixConn, req *msg.Request) error
func (d *childDriver) handleConnectRequest(c *net.UnixConn, req *msg.Request) error { func (d *childDriver) handleConnectRequest(c *net.UnixConn, req *msg.Request) error {
switch req.Proto { switch req.Proto {
case "tcp": case "tcp":
case "tcp4":
case "tcp6":
case "udp": case "udp":
case "udp4":
case "udp6":
default: default:
return errors.Errorf("unknown proto: %q", req.Proto) return errors.Errorf("unknown proto: %q", req.Proto)
} }
// dialProto does not need "4", "6" suffix
dialProto := strings.TrimSuffix(strings.TrimSuffix(req.Proto, "6"), "4")
var dialer net.Dialer var dialer net.Dialer
targetConn, err := dialer.Dial(req.Proto, fmt.Sprintf("127.0.0.1:%d", req.Port)) ip := req.IP
if ip == "" {
ip = "127.0.0.1"
} else {
p := net.ParseIP(ip)
if p == nil {
return errors.Errorf("invalid IP: %q", ip)
}
ip = p.String()
}
targetConn, err := dialer.Dial(dialProto, net.JoinHostPort(ip, strconv.Itoa(req.Port)))
if err != nil { if err != nil {
return err return err
} }
......
...@@ -19,7 +19,8 @@ const ( ...@@ -19,7 +19,8 @@ const (
// Request and Response are encoded as JSON with uint32le length header. // Request and Response are encoded as JSON with uint32le length header.
type Request struct { type Request struct {
Type string // "init" or "connect" Type string // "init" or "connect"
Proto string // "tcp" or "udp" Proto string // "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6"
IP string
Port int Port int
} }
...@@ -53,6 +54,7 @@ func ConnectToChild(c *net.UnixConn, spec port.Spec) (int, error) { ...@@ -53,6 +54,7 @@ func ConnectToChild(c *net.UnixConn, spec port.Spec) (int, error) {
Type: RequestTypeConnect, Type: RequestTypeConnect,
Proto: spec.Proto, Proto: spec.Proto,
Port: spec.ChildPort, Port: spec.ChildPort,
IP: spec.ChildIP,
} }
if _, err := msgutil.MarshalToWriter(c, &req); err != nil { if _, err := msgutil.MarshalToWriter(c, &req); err != nil {
return 0, err return 0, err
......
...@@ -12,9 +12,11 @@ import ( ...@@ -12,9 +12,11 @@ import (
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rootless-containers/rootlesskit/pkg/api"
"github.com/rootless-containers/rootlesskit/pkg/port" "github.com/rootless-containers/rootlesskit/pkg/port"
"github.com/rootless-containers/rootlesskit/pkg/port/builtin/msg" "github.com/rootless-containers/rootlesskit/pkg/port/builtin/msg"
"github.com/rootless-containers/rootlesskit/pkg/port/builtin/opaque" "github.com/rootless-containers/rootlesskit/pkg/port/builtin/opaque"
...@@ -40,7 +42,7 @@ func NewDriver(logWriter io.Writer, stateDir string) (port.ParentDriver, error) ...@@ -40,7 +42,7 @@ func NewDriver(logWriter io.Writer, stateDir string) (port.ParentDriver, error)
socketPath: socketPath, socketPath: socketPath,
childReadyPipePath: childReadyPipePath, childReadyPipePath: childReadyPipePath,
ports: make(map[int]*port.Status, 0), ports: make(map[int]*port.Status, 0),
stoppers: make(map[int]func() error, 0), stoppers: make(map[int]func(context.Context) error, 0),
nextID: 1, nextID: 1,
} }
return &d, nil return &d, nil
...@@ -52,10 +54,19 @@ type driver struct { ...@@ -52,10 +54,19 @@ type driver struct {
childReadyPipePath string childReadyPipePath string
mu sync.Mutex mu sync.Mutex
ports map[int]*port.Status ports map[int]*port.Status
stoppers map[int]func() error stoppers map[int]func(context.Context) error
nextID int nextID int
} }
func (d *driver) Info(ctx context.Context) (*api.PortDriverInfo, error) {
info := &api.PortDriverInfo{
Driver: "builtin",
Protos: []string{"tcp", "tcp4", "tcp6", "udp", "udp4", "udp6"},
DisallowLoopbackChildIP: false,
}
return info, nil
}
func (d *driver) OpaqueForChild() map[string]string { func (d *driver) OpaqueForChild() map[string]string {
return map[string]string{ return map[string]string{
opaque.SocketPath: d.socketPath, opaque.SocketPath: d.socketPath,
...@@ -111,7 +122,7 @@ func annotateEPERM(origErr error, spec port.Spec) error { ...@@ -111,7 +122,7 @@ func annotateEPERM(origErr error, spec port.Spec) error {
// origErr is unrelated to ip_unprivileged_port_start // origErr is unrelated to ip_unprivileged_port_start
return origErr return origErr
} }
text := fmt.Sprintf("cannot expose privileged port %d, you might need to add \"net.ipv4.ip_unprivileged_port_start=0\" (currently %d) to /etc/sysctl.conf", spec.ParentPort, start) text := fmt.Sprintf("cannot expose privileged port %d, you can add 'net.ipv4.ip_unprivileged_port_start=%d' to /etc/sysctl.conf (currently %d)", spec.ParentPort, spec.ParentPort, start)
if filepath.Base(os.Args[0]) == "rootlesskit" { if filepath.Base(os.Args[0]) == "rootlesskit" {
// NOTE: The following sentence is appended only if Args[0] == "rootlesskit", because it does not apply to Podman (as of Podman v1.9). // NOTE: The following sentence is appended only if Args[0] == "rootlesskit", because it does not apply to Podman (as of Podman v1.9).
// Podman launches the parent driver in the child user namespace (but in the parent network namespace), which disables the file capability. // Podman launches the parent driver in the child user namespace (but in the parent network namespace), which disables the file capability.
...@@ -128,16 +139,27 @@ func (d *driver) AddPort(ctx context.Context, spec port.Spec) (*port.Status, err ...@@ -128,16 +139,27 @@ func (d *driver) AddPort(ctx context.Context, spec port.Spec) (*port.Status, err
if err != nil { if err != nil {
return nil, err return nil, err
} }
// NOTE: routineStopCh is close-only channel. Do not send any data.
// See commit 4803f18fae1e39d200d98f09e445a97ccd6f5526 `Revert "port/builtin: RemovePort() block until conn is closed"`
routineStopCh := make(chan struct{}) routineStopCh := make(chan struct{})
routineStop := func() error { routineStoppedCh := make(chan error)
routineStop := func(ctx context.Context) error {
close(routineStopCh) close(routineStopCh)
return nil // FIXME select {
case stoppedResult, stoppedResultOk := <-routineStoppedCh:
if stoppedResultOk {
return stoppedResult
}
return errors.New("routineStoppedCh was closed without sending data?")
case <-ctx.Done():
return errors.Wrap(err, "timed out while waiting for routineStoppedCh after closing routineStopCh")
}
} }
switch spec.Proto { switch spec.Proto {
case "tcp": case "tcp", "tcp4", "tcp6":
err = tcp.Run(d.socketPath, spec, routineStopCh, d.logWriter) err = tcp.Run(d.socketPath, spec, routineStopCh, routineStoppedCh, d.logWriter)
case "udp": case "udp", "udp4", "udp6":
err = udp.Run(d.socketPath, spec, routineStopCh, d.logWriter) err = udp.Run(d.socketPath, spec, routineStopCh, routineStoppedCh, d.logWriter)
default: default:
// NOTREACHED // NOTREACHED
return nil, errors.New("spec was not validated?") return nil, errors.New("spec was not validated?")
...@@ -178,7 +200,12 @@ func (d *driver) RemovePort(ctx context.Context, id int) error { ...@@ -178,7 +200,12 @@ func (d *driver) RemovePort(ctx context.Context, id int) error {
if !ok { if !ok {
return errors.Errorf("unknown id: %d", id) return errors.Errorf("unknown id: %d", id)
} }
err := stop() if _, ok := ctx.Deadline(); !ok {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, 5*time.Second)
defer cancel()
}
err := stop(ctx)
delete(d.stoppers, id) delete(d.stoppers, id)
delete(d.ports, id) delete(d.ports, id)
return err return err
......
...@@ -5,14 +5,15 @@ import ( ...@@ -5,14 +5,15 @@ import (
"io" "io"
"net" "net"
"os" "os"
"strconv"
"sync" "sync"
"github.com/rootless-containers/rootlesskit/pkg/port" "github.com/rootless-containers/rootlesskit/pkg/port"
"github.com/rootless-containers/rootlesskit/pkg/port/builtin/msg" "github.com/rootless-containers/rootlesskit/pkg/port/builtin/msg"
) )
func Run(socketPath string, spec port.Spec, stopCh <-chan struct{}, logWriter io.Writer) error { func Run(socketPath string, spec port.Spec, stopCh <-chan struct{}, stoppedCh chan error, logWriter io.Writer) error {
ln, err := net.Listen("tcp", fmt.Sprintf("%s:%d", spec.ParentIP, spec.ParentPort)) ln, err := net.Listen(spec.Proto, net.JoinHostPort(spec.ParentIP, strconv.Itoa(spec.ParentPort)))
if err != nil { if err != nil {
fmt.Fprintf(logWriter, "listen: %v\n", err) fmt.Fprintf(logWriter, "listen: %v\n", err)
return err return err
...@@ -30,7 +31,10 @@ func Run(socketPath string, spec port.Spec, stopCh <-chan struct{}, logWriter io ...@@ -30,7 +31,10 @@ func Run(socketPath string, spec port.Spec, stopCh <-chan struct{}, logWriter io
} }
}() }()
go func() { go func() {
defer ln.Close() defer func() {
stoppedCh <- ln.Close()
close(stoppedCh)
}()
for { for {
select { select {
case c, ok := <-newConns: case c, ok := <-newConns:
......
package udp package udp
import ( import (
"fmt"
"io" "io"
"net" "net"
"os" "os"
"strconv"
"github.com/pkg/errors" "github.com/pkg/errors"
...@@ -13,12 +13,12 @@ import ( ...@@ -13,12 +13,12 @@ import (
"github.com/rootless-containers/rootlesskit/pkg/port/builtin/parent/udp/udpproxy" "github.com/rootless-containers/rootlesskit/pkg/port/builtin/parent/udp/udpproxy"
) )
func Run(socketPath string, spec port.Spec, stopCh <-chan struct{}, logWriter io.Writer) error { func Run(socketPath string, spec port.Spec, stopCh <-chan struct{}, stoppedCh chan error, logWriter io.Writer) error {
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", spec.ParentIP, spec.ParentPort)) addr, err := net.ResolveUDPAddr(spec.Proto, net.JoinHostPort(spec.ParentIP, strconv.Itoa(spec.ParentPort)))
if err != nil { if err != nil {
return err return err
} }
c, err := net.ListenUDP("udp", addr) c, err := net.ListenUDP(spec.Proto, addr)
if err != nil { if err != nil {
return err return err
} }
...@@ -51,6 +51,8 @@ func Run(socketPath string, spec port.Spec, stopCh <-chan struct{}, logWriter io ...@@ -51,6 +51,8 @@ func Run(socketPath string, spec port.Spec, stopCh <-chan struct{}, logWriter io
case <-stopCh: case <-stopCh:
// udpp.Close closes ln as well // udpp.Close closes ln as well
udpp.Close() udpp.Close()
stoppedCh <- nil
close(stoppedCh)
return return
} }
} }
......
...@@ -3,13 +3,22 @@ package port ...@@ -3,13 +3,22 @@ package port
import ( import (
"context" "context"
"net" "net"
"github.com/rootless-containers/rootlesskit/pkg/api"
) )
type Spec struct { type Spec struct {
Proto string `json:"proto,omitempty"` // either "tcp" or "udp". in future "sctp" will be supported as well. // Proto is one of ["tcp", "tcp4", "tcp6", "udp", "udp4", "udp6"].
ParentIP string `json:"parentIP,omitempty"` // IPv4 address. can be empty (0.0.0.0). // "tcp" may cause listening on both IPv4 and IPv6. (Corresponds to Go's net.Listen .)
Proto string `json:"proto,omitempty"`
ParentIP string `json:"parentIP,omitempty"` // IPv4 or IPv6 address. can be empty (0.0.0.0).
ParentPort int `json:"parentPort,omitempty"` ParentPort int `json:"parentPort,omitempty"`
ChildPort int `json:"childPort,omitempty"` ChildPort int `json:"childPort,omitempty"`
// ChildIP is an IPv4 or IPv6 address.
// Default values:
// - builtin driver: 127.0.0.1
// - slirp4netns driver: slirp4netns's child IP, e.g., 10.0.2.100
ChildIP string `json:"childIP,omitempty"`
} }
type Status struct { type Status struct {
...@@ -35,6 +44,7 @@ type ChildContext struct { ...@@ -35,6 +44,7 @@ type ChildContext struct {
// ParentDriver is a driver for the parent process. // ParentDriver is a driver for the parent process.
type ParentDriver interface { type ParentDriver interface {
Manager Manager
Info(ctx context.Context) (*api.PortDriverInfo, error)
// OpaqueForChild typically consists of socket path // OpaqueForChild typically consists of socket path
// for controlling child from parent // for controlling child from parent
OpaqueForChild() map[string]string OpaqueForChild() map[string]string
......
...@@ -2,52 +2,137 @@ package portutil ...@@ -2,52 +2,137 @@ package portutil
import ( import (
"net" "net"
"regexp"
"strconv" "strconv"
"strings"
"text/scanner"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rootless-containers/rootlesskit/pkg/port" "github.com/rootless-containers/rootlesskit/pkg/port"
) )
// ParsePortSpec parses a Docker-like representation of PortSpec. // ParsePortSpec parses a Docker-like representation of PortSpec, but with
// e.g. "127.0.0.1:8080:80/tcp" // support for both "parent IP" and "child IP" (optional);
func ParsePortSpec(s string) (*port.Spec, error) { // e.g. "127.0.0.1:8080:80/tcp", or "127.0.0.1:8080:10.0.2.100:80/tcp"
r := regexp.MustCompile("^([0-9a-f\\.]+):([0-9]+):([0-9]+)/([a-z]+)$") //
g := r.FindStringSubmatch(s) // Format is as follows:
if len(g) != 5 { //
return nil, errors.Errorf("unexpected PortSpec string: %q", s) // <parent IP>:<parent port>[:<child IP>]:<child port>/<proto>
//
// Note that (child IP being optional) the format can either contain 5 or 4
// components. When using IPv6 IP addresses, addresses must use square brackets
// to prevent the colons being mistaken for delimiters. For example:
//
// [::1]:8080:[::2]:80/udp
func ParsePortSpec(portSpec string) (*port.Spec, error) {
const (
parentIP = iota
parentPort = iota
childIP = iota
childPort = iota
proto = iota
)
var (
s scanner.Scanner
err error
parts = make([]string, 5)
index = parentIP
delimiter = ':'
)
// First get the "proto" and "parent-port" at the end. These parts are
// required, whereas "ParentIP" is optional. Removing them first makes
// it easier to parse the remaining parts, as otherwise the third part
// could be _either_ an IP-address _or_ a Port.
// Get the proto
protoPos := strings.LastIndex(portSpec, "/")
if protoPos < 0 {
return nil, errors.Errorf("missing proto in PortSpec string: %q", portSpec)
} }
parentIP := g[1] parts[proto] = portSpec[protoPos+1:]
parentPort, err := strconv.Atoi(g[2]) err = validateProto(parts[proto])
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "unexpected ParentPort in PortSpec string: %q", s) return nil, errors.Wrapf(err, "invalid PortSpec string: %q", portSpec)
}
// Get the parent port
portPos := strings.LastIndex(portSpec, ":")
if portPos < 0 {
return nil, errors.Errorf("unexpected PortSpec string: %q", portSpec)
}
parts[childPort] = portSpec[portPos+1 : protoPos]
// Scan the remainder "<IP-address>:<port>[:<IP-address>]"
s.Init(strings.NewReader(portSpec[:portPos]))
for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
if index > childPort {
return nil, errors.Errorf("unexpected PortSpec string: %q", portSpec)
}
switch tok {
case '[':
// Start of IPv6 IP-address; value ends at closing bracket (])
delimiter = ']'
continue
case delimiter:
if delimiter == ']' {
// End of IPv6 IP-address
delimiter = ':'
// Skip the next token, which should be a colon delimiter (:)
tok = s.Scan()
}
index++
continue
default:
parts[index] += s.TokenText()
}
}
if parts[parentIP] != "" && net.ParseIP(parts[parentIP]) == nil {
return nil, errors.Errorf("unexpected ParentIP in PortSpec string: %q", portSpec)
}
if parts[childIP] != "" && net.ParseIP(parts[childIP]) == nil {
return nil, errors.Errorf("unexpected ParentIP in PortSpec string: %q", portSpec)
} }
childPort, err := strconv.Atoi(g[3])
ps := &port.Spec{
Proto: parts[proto],
ParentIP: parts[parentIP],
ChildIP: parts[childIP],
}
ps.ParentPort, err = strconv.Atoi(parts[parentPort])
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "unexpected ChildPort in PortSpec string: %q", s) return nil, errors.Wrapf(err, "unexpected ChildPort in PortSpec string: %q", portSpec)
} }
proto := g[4]
// validation is up to the caller (as json.Unmarshal doesn't validate values) ps.ChildPort, err = strconv.Atoi(parts[childPort])
return &port.Spec{ if err != nil {
Proto: proto, return nil, errors.Wrapf(err, "unexpected ParentPort in PortSpec string: %q", portSpec)
ParentIP: parentIP, }
ParentPort: parentPort,
ChildPort: childPort, return ps, nil
}, nil
} }
// ValidatePortSpec validates *port.Spec. // ValidatePortSpec validates *port.Spec.
// existingPorts can be optionally passed for detecting conflicts. // existingPorts can be optionally passed for detecting conflicts.
func ValidatePortSpec(spec port.Spec, existingPorts map[int]*port.Status) error { func ValidatePortSpec(spec port.Spec, existingPorts map[int]*port.Status) error {
if spec.Proto != "tcp" && spec.Proto != "udp" { if err := validateProto(spec.Proto); err != nil {
return errors.Errorf("unknown proto: %q", spec.Proto) return err
} }
if spec.ParentIP != "" { if spec.ParentIP != "" {
if net.ParseIP(spec.ParentIP) == nil { if net.ParseIP(spec.ParentIP) == nil {
return errors.Errorf("invalid ParentIP: %q", spec.ParentIP) return errors.Errorf("invalid ParentIP: %q", spec.ParentIP)
} }
} }
if spec.ChildIP != "" {
if net.ParseIP(spec.ChildIP) == nil {
return errors.Errorf("invalid ChildIP: %q", spec.ChildIP)
}
}
if spec.ParentPort <= 0 || spec.ParentPort > 65535 { if spec.ParentPort <= 0 || spec.ParentPort > 65535 {
return errors.Errorf("invalid ParentPort: %q", spec.ParentPort) return errors.Errorf("invalid ParentPort: %q", spec.ParentPort)
} }
...@@ -64,3 +149,15 @@ func ValidatePortSpec(spec port.Spec, existingPorts map[int]*port.Status) error ...@@ -64,3 +149,15 @@ func ValidatePortSpec(spec port.Spec, existingPorts map[int]*port.Status) error
} }
return nil return nil
} }
func validateProto(proto string) error {
switch proto {
case
"tcp", "tcp4", "tcp6",
"udp", "udp4", "udp6",
"sctp", "sctp4", "sctp6":
return nil
default:
return errors.Errorf("unknown proto: %q", proto)
}
}
Blackfriday [![Build Status](https://travis-ci.org/russross/blackfriday.svg?branch=master)](https://travis-ci.org/russross/blackfriday) Blackfriday
[![Build Status][BuildV2SVG]][BuildV2URL]
[![PkgGoDev][PkgGoDevV2SVG]][PkgGoDevV2URL]
=========== ===========
Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It
...@@ -16,19 +18,21 @@ It started as a translation from C of [Sundown][3]. ...@@ -16,19 +18,21 @@ It started as a translation from C of [Sundown][3].
Installation Installation
------------ ------------
Blackfriday is compatible with any modern Go release. With Go 1.7 and git Blackfriday is compatible with modern Go releases in module mode.
installed: With Go installed:
go get gopkg.in/russross/blackfriday.v2 go get github.com/russross/blackfriday/v2
will download, compile, and install the package into your `$GOPATH` will resolve and add the package to the current development module,
directory hierarchy. Alternatively, you can achieve the same if you then build and install it. Alternatively, you can achieve the same
import it into a project: if you import it in a package:
import "gopkg.in/russross/blackfriday.v2" import "github.com/russross/blackfriday/v2"
and `go get` without parameters. and `go get` without parameters.
Legacy GOPATH mode is unsupported.
Versions Versions
-------- --------
...@@ -36,13 +40,9 @@ Versions ...@@ -36,13 +40,9 @@ Versions
Currently maintained and recommended version of Blackfriday is `v2`. It's being Currently maintained and recommended version of Blackfriday is `v2`. It's being
developed on its own branch: https://github.com/russross/blackfriday/tree/v2 and the developed on its own branch: https://github.com/russross/blackfriday/tree/v2 and the
documentation is available at documentation is available at
https://godoc.org/gopkg.in/russross/blackfriday.v2. https://pkg.go.dev/github.com/russross/blackfriday/v2.
It is `go get`-able via via [gopkg.in][6] at `gopkg.in/russross/blackfriday.v2`, It is `go get`-able in module mode at `github.com/russross/blackfriday/v2`.
but we highly recommend using package management tool like [dep][7] or
[Glide][8] and make use of semantic versioning. With package management you
should import `github.com/russross/blackfriday` and specify that you're using
version 2.0.0.
Version 2 offers a number of improvements over v1: Version 2 offers a number of improvements over v1:
...@@ -62,6 +62,11 @@ Potential drawbacks: ...@@ -62,6 +62,11 @@ Potential drawbacks:
v2. See issue [#348](https://github.com/russross/blackfriday/issues/348) for v2. See issue [#348](https://github.com/russross/blackfriday/issues/348) for
tracking. tracking.
If you are still interested in the legacy `v1`, you can import it from
`github.com/russross/blackfriday`. Documentation for the legacy v1 can be found
here: https://pkg.go.dev/github.com/russross/blackfriday.
Usage Usage
----- -----
...@@ -91,7 +96,7 @@ Here's an example of simple usage of Blackfriday together with Bluemonday: ...@@ -91,7 +96,7 @@ Here's an example of simple usage of Blackfriday together with Bluemonday:
```go ```go
import ( import (
"github.com/microcosm-cc/bluemonday" "github.com/microcosm-cc/bluemonday"
"github.com/russross/blackfriday" "github.com/russross/blackfriday/v2"
) )
// ... // ...
...@@ -104,6 +109,8 @@ html := bluemonday.UGCPolicy().SanitizeBytes(unsafe) ...@@ -104,6 +109,8 @@ html := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
If you want to customize the set of options, use `blackfriday.WithExtensions`, If you want to customize the set of options, use `blackfriday.WithExtensions`,
`blackfriday.WithRenderer` and `blackfriday.WithRefOverride`. `blackfriday.WithRenderer` and `blackfriday.WithRefOverride`.
### `blackfriday-tool`
You can also check out `blackfriday-tool` for a more complete example You can also check out `blackfriday-tool` for a more complete example
of how to use it. Download and install it using: of how to use it. Download and install it using:
...@@ -114,7 +121,7 @@ markdown file using a standalone program. You can also browse the ...@@ -114,7 +121,7 @@ markdown file using a standalone program. You can also browse the
source directly on github if you are just looking for some example source directly on github if you are just looking for some example
code: code:
* <http://github.com/russross/blackfriday-tool> * <https://github.com/russross/blackfriday-tool>
Note that if you have not already done so, installing Note that if you have not already done so, installing
`blackfriday-tool` will be sufficient to download and install `blackfriday-tool` will be sufficient to download and install
...@@ -123,6 +130,22 @@ installed in `$GOPATH/bin`. This is a statically-linked binary that ...@@ -123,6 +130,22 @@ installed in `$GOPATH/bin`. This is a statically-linked binary that
can be copied to wherever you need it without worrying about can be copied to wherever you need it without worrying about
dependencies and library versions. dependencies and library versions.
### Sanitized anchor names
Blackfriday includes an algorithm for creating sanitized anchor names
corresponding to a given input text. This algorithm is used to create
anchors for headings when `AutoHeadingIDs` extension is enabled. The
algorithm has a specification, so that other packages can create
compatible anchor names and links to those anchors.
The specification is located at https://pkg.go.dev/github.com/russross/blackfriday/v2#hdr-Sanitized_Anchor_Names.
[`SanitizedAnchorName`](https://pkg.go.dev/github.com/russross/blackfriday/v2#SanitizedAnchorName) exposes this functionality, and can be used to
create compatible links to the anchor names generated by blackfriday.
This algorithm is also implemented in a small standalone package at
[`github.com/shurcooL/sanitized_anchor_name`](https://pkg.go.dev/github.com/shurcooL/sanitized_anchor_name). It can be useful for clients
that want a small package and don't need full functionality of blackfriday.
Features Features
-------- --------
...@@ -199,6 +222,15 @@ implements the following extensions: ...@@ -199,6 +222,15 @@ implements the following extensions:
You can use 3 or more backticks to mark the beginning of the You can use 3 or more backticks to mark the beginning of the
block, and the same number to mark the end of the block. block, and the same number to mark the end of the block.
To preserve classes of fenced code blocks while using the bluemonday
HTML sanitizer, use the following policy:
```go
p := bluemonday.UGCPolicy()
p.AllowAttrs("class").Matching(regexp.MustCompile("^language-[a-zA-Z0-9]+$")).OnElements("code")
html := p.SanitizeBytes(unsafe)
```
* **Definition lists**. A simple definition list is made of a single-line * **Definition lists**. A simple definition list is made of a single-line
term followed by a colon and the definition for that term. term followed by a colon and the definition for that term.
...@@ -250,7 +282,7 @@ Other renderers ...@@ -250,7 +282,7 @@ Other renderers
Blackfriday is structured to allow alternative rendering engines. Here Blackfriday is structured to allow alternative rendering engines. Here
are a few of note: are a few of note:
* [github_flavored_markdown](https://godoc.org/github.com/shurcooL/github_flavored_markdown): * [github_flavored_markdown](https://pkg.go.dev/github.com/shurcooL/github_flavored_markdown):
provides a GitHub Flavored Markdown renderer with fenced code block provides a GitHub Flavored Markdown renderer with fenced code block
highlighting, clickable heading anchor links. highlighting, clickable heading anchor links.
...@@ -261,20 +293,28 @@ are a few of note: ...@@ -261,20 +293,28 @@ are a few of note:
* [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt, * [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt,
but for markdown. but for markdown.
* [LaTeX output](https://github.com/Ambrevar/Blackfriday-LaTeX): * [LaTeX output](https://gitlab.com/ambrevar/blackfriday-latex):
renders output as LaTeX. renders output as LaTeX.
* [bfchroma](https://github.com/Depado/bfchroma/): provides convenience
integration with the [Chroma](https://github.com/alecthomas/chroma) code
highlighting library. bfchroma is only compatible with v2 of Blackfriday and
provides a drop-in renderer ready to use with Blackfriday, as well as
options and means for further customization.
* [Blackfriday-Confluence](https://github.com/kentaro-m/blackfriday-confluence): provides a [Confluence Wiki Markup](https://confluence.atlassian.com/doc/confluence-wiki-markup-251003035.html) renderer. * [Blackfriday-Confluence](https://github.com/kentaro-m/blackfriday-confluence): provides a [Confluence Wiki Markup](https://confluence.atlassian.com/doc/confluence-wiki-markup-251003035.html) renderer.
* [Blackfriday-Slack](https://github.com/karriereat/blackfriday-slack): converts markdown to slack message style
Todo TODO
---- ----
* More unit testing * More unit testing
* Improve unicode support. It does not understand all unicode * Improve Unicode support. It does not understand all Unicode
rules (about what constitutes a letter, a punctuation symbol, rules (about what constitutes a letter, a punctuation symbol,
etc.), so it may fail to detect word boundaries correctly in etc.), so it may fail to detect word boundaries correctly in
some instances. It is safe on all utf-8 input. some instances. It is safe on all UTF-8 input.
License License
...@@ -286,6 +326,10 @@ License ...@@ -286,6 +326,10 @@ License
[1]: https://daringfireball.net/projects/markdown/ "Markdown" [1]: https://daringfireball.net/projects/markdown/ "Markdown"
[2]: https://golang.org/ "Go Language" [2]: https://golang.org/ "Go Language"
[3]: https://github.com/vmg/sundown "Sundown" [3]: https://github.com/vmg/sundown "Sundown"
[4]: https://godoc.org/gopkg.in/russross/blackfriday.v2#Parse "Parse func" [4]: https://pkg.go.dev/github.com/russross/blackfriday/v2#Parse "Parse func"
[5]: https://github.com/microcosm-cc/bluemonday "Bluemonday" [5]: https://github.com/microcosm-cc/bluemonday "Bluemonday"
[6]: https://labix.org/gopkg.in "gopkg.in"
[BuildV2SVG]: https://travis-ci.org/russross/blackfriday.svg?branch=v2
[BuildV2URL]: https://travis-ci.org/russross/blackfriday
[PkgGoDevV2SVG]: https://pkg.go.dev/badge/github.com/russross/blackfriday/v2
[PkgGoDevV2URL]: https://pkg.go.dev/github.com/russross/blackfriday/v2
...@@ -18,8 +18,7 @@ import ( ...@@ -18,8 +18,7 @@ import (
"html" "html"
"regexp" "regexp"
"strings" "strings"
"unicode"
"github.com/shurcooL/sanitized_anchor_name"
) )
const ( const (
...@@ -259,7 +258,7 @@ func (p *Markdown) prefixHeading(data []byte) int { ...@@ -259,7 +258,7 @@ func (p *Markdown) prefixHeading(data []byte) int {
} }
if end > i { if end > i {
if id == "" && p.extensions&AutoHeadingIDs != 0 { if id == "" && p.extensions&AutoHeadingIDs != 0 {
id = sanitized_anchor_name.Create(string(data[i:end])) id = SanitizedAnchorName(string(data[i:end]))
} }
block := p.addBlock(Heading, data[i:end]) block := p.addBlock(Heading, data[i:end])
block.HeadingID = id block.HeadingID = id
...@@ -673,6 +672,7 @@ func (p *Markdown) fencedCodeBlock(data []byte, doRender bool) int { ...@@ -673,6 +672,7 @@ func (p *Markdown) fencedCodeBlock(data []byte, doRender bool) int {
if beg == 0 || beg >= len(data) { if beg == 0 || beg >= len(data) {
return 0 return 0
} }
fenceLength := beg - 1
var work bytes.Buffer var work bytes.Buffer
work.Write([]byte(info)) work.Write([]byte(info))
...@@ -706,6 +706,7 @@ func (p *Markdown) fencedCodeBlock(data []byte, doRender bool) int { ...@@ -706,6 +706,7 @@ func (p *Markdown) fencedCodeBlock(data []byte, doRender bool) int {
if doRender { if doRender {
block := p.addBlock(CodeBlock, work.Bytes()) // TODO: get rid of temp buffer block := p.addBlock(CodeBlock, work.Bytes()) // TODO: get rid of temp buffer
block.IsFenced = true block.IsFenced = true
block.FenceLength = fenceLength
finalizeCodeBlock(block) finalizeCodeBlock(block)
} }
...@@ -1503,7 +1504,7 @@ func (p *Markdown) paragraph(data []byte) int { ...@@ -1503,7 +1504,7 @@ func (p *Markdown) paragraph(data []byte) int {
id := "" id := ""
if p.extensions&AutoHeadingIDs != 0 { if p.extensions&AutoHeadingIDs != 0 {
id = sanitized_anchor_name.Create(string(data[prev:eol])) id = SanitizedAnchorName(string(data[prev:eol]))
} }
block := p.addBlock(Heading, data[prev:eol]) block := p.addBlock(Heading, data[prev:eol])
...@@ -1588,3 +1589,24 @@ func skipUntilChar(text []byte, start int, char byte) int { ...@@ -1588,3 +1589,24 @@ func skipUntilChar(text []byte, start int, char byte) int {
} }
return i return i
} }
// SanitizedAnchorName returns a sanitized anchor name for the given text.
//
// It implements the algorithm specified in the package comment.
func SanitizedAnchorName(text string) string {
var anchorName []rune
futureDash := false
for _, r := range text {
switch {
case unicode.IsLetter(r) || unicode.IsNumber(r):
if futureDash && len(anchorName) > 0 {
anchorName = append(anchorName, '-')
}
futureDash = false
anchorName = append(anchorName, unicode.ToLower(r))
default:
futureDash = true
}
}
return string(anchorName)
}
...@@ -15,4 +15,32 @@ ...@@ -15,4 +15,32 @@
// //
// If you're interested in calling Blackfriday from command line, see // If you're interested in calling Blackfriday from command line, see
// https://github.com/russross/blackfriday-tool. // https://github.com/russross/blackfriday-tool.
//
// Sanitized Anchor Names
//
// Blackfriday includes an algorithm for creating sanitized anchor names
// corresponding to a given input text. This algorithm is used to create
// anchors for headings when AutoHeadingIDs extension is enabled. The
// algorithm is specified below, so that other packages can create
// compatible anchor names and links to those anchors.
//
// The algorithm iterates over the input text, interpreted as UTF-8,
// one Unicode code point (rune) at a time. All runes that are letters (category L)
// or numbers (category N) are considered valid characters. They are mapped to
// lower case, and included in the output. All other runes are considered
// invalid characters. Invalid characters that precede the first valid character,
// as well as invalid character that follow the last valid character
// are dropped completely. All other sequences of invalid characters
// between two valid characters are replaced with a single dash character '-'.
//
// SanitizedAnchorName exposes this functionality, and can be used to
// create compatible links to the anchor names generated by blackfriday.
// This algorithm is also implemented in a small standalone package at
// github.com/shurcooL/sanitized_anchor_name. It can be useful for clients
// that want a small package and don't need full functionality of blackfriday.
package blackfriday package blackfriday
// NOTE: Keep Sanitized Anchor Name algorithm in sync with package
// github.com/shurcooL/sanitized_anchor_name.
// Otherwise, users of sanitized_anchor_name will get anchor names
// that are incompatible with those generated by blackfriday.
...@@ -13,13 +13,27 @@ var htmlEscaper = [256][]byte{ ...@@ -13,13 +13,27 @@ var htmlEscaper = [256][]byte{
} }
func escapeHTML(w io.Writer, s []byte) { func escapeHTML(w io.Writer, s []byte) {
escapeEntities(w, s, false)
}
func escapeAllHTML(w io.Writer, s []byte) {
escapeEntities(w, s, true)
}
func escapeEntities(w io.Writer, s []byte, escapeValidEntities bool) {
var start, end int var start, end int
for end < len(s) { for end < len(s) {
escSeq := htmlEscaper[s[end]] escSeq := htmlEscaper[s[end]]
if escSeq != nil { if escSeq != nil {
w.Write(s[start:end]) isEntity, entityEnd := nodeIsEntity(s, end)
w.Write(escSeq) if isEntity && !escapeValidEntities {
start = end + 1 w.Write(s[start : entityEnd+1])
start = entityEnd + 1
} else {
w.Write(s[start:end])
w.Write(escSeq)
start = end + 1
}
} }
end++ end++
} }
...@@ -28,6 +42,28 @@ func escapeHTML(w io.Writer, s []byte) { ...@@ -28,6 +42,28 @@ func escapeHTML(w io.Writer, s []byte) {
} }
} }
func nodeIsEntity(s []byte, end int) (isEntity bool, endEntityPos int) {
isEntity = false
endEntityPos = end + 1
if s[end] == '&' {
for endEntityPos < len(s) {
if s[endEntityPos] == ';' {
if entities[string(s[end:endEntityPos+1])] {
isEntity = true
break
}
}
if !isalnum(s[endEntityPos]) && s[endEntityPos] != '&' && s[endEntityPos] != '#' {
break
}
endEntityPos++
}
}
return isEntity, endEntityPos
}
func escLink(w io.Writer, text []byte) { func escLink(w io.Writer, text []byte) {
unesc := html.UnescapeString(string(text)) unesc := html.UnescapeString(string(text))
escapeHTML(w, []byte(unesc)) escapeHTML(w, []byte(unesc))
......
...@@ -132,7 +132,10 @@ func NewHTMLRenderer(params HTMLRendererParameters) *HTMLRenderer { ...@@ -132,7 +132,10 @@ func NewHTMLRenderer(params HTMLRendererParameters) *HTMLRenderer {
} }
if params.FootnoteReturnLinkContents == "" { if params.FootnoteReturnLinkContents == "" {
params.FootnoteReturnLinkContents = `<sup>[return]</sup>` // U+FE0E is VARIATION SELECTOR-15.
// It suppresses automatic emoji presentation of the preceding
// U+21A9 LEFTWARDS ARROW WITH HOOK on iOS and iPadOS.
params.FootnoteReturnLinkContents = "<span aria-label='Return'>↩\ufe0e</span>"
} }
return &HTMLRenderer{ return &HTMLRenderer{
...@@ -616,7 +619,7 @@ func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkSt ...@@ -616,7 +619,7 @@ func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkSt
} }
case Code: case Code:
r.out(w, codeTag) r.out(w, codeTag)
escapeHTML(w, node.Literal) escapeAllHTML(w, node.Literal)
r.out(w, codeCloseTag) r.out(w, codeCloseTag)
case Document: case Document:
break break
...@@ -762,7 +765,7 @@ func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkSt ...@@ -762,7 +765,7 @@ func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkSt
r.cr(w) r.cr(w)
r.out(w, preTag) r.out(w, preTag)
r.tag(w, codeTag[:len(codeTag)-1], attrs) r.tag(w, codeTag[:len(codeTag)-1], attrs)
escapeHTML(w, node.Literal) escapeAllHTML(w, node.Literal)
r.out(w, codeCloseTag) r.out(w, codeCloseTag)
r.out(w, preCloseTag) r.out(w, preCloseTag)
if node.Parent.Type != Item { if node.Parent.Type != Item {
......
...@@ -278,7 +278,7 @@ func link(p *Markdown, data []byte, offset int) (int, *Node) { ...@@ -278,7 +278,7 @@ func link(p *Markdown, data []byte, offset int) (int, *Node) {
case data[i] == '\n': case data[i] == '\n':
textHasNl = true textHasNl = true
case data[i-1] == '\\': case isBackslashEscaped(data, i):
continue continue
case data[i] == '[': case data[i] == '[':
......
...@@ -199,7 +199,8 @@ func (n *Node) InsertBefore(sibling *Node) { ...@@ -199,7 +199,8 @@ func (n *Node) InsertBefore(sibling *Node) {
} }
} }
func (n *Node) isContainer() bool { // IsContainer returns true if 'n' can contain children.
func (n *Node) IsContainer() bool {
switch n.Type { switch n.Type {
case Document: case Document:
fallthrough fallthrough
...@@ -238,6 +239,11 @@ func (n *Node) isContainer() bool { ...@@ -238,6 +239,11 @@ func (n *Node) isContainer() bool {
} }
} }
// IsLeaf returns true if 'n' is a leaf node.
func (n *Node) IsLeaf() bool {
return !n.IsContainer()
}
func (n *Node) canContain(t NodeType) bool { func (n *Node) canContain(t NodeType) bool {
if n.Type == List { if n.Type == List {
return t == Item return t == Item
...@@ -309,11 +315,11 @@ func newNodeWalker(root *Node) *nodeWalker { ...@@ -309,11 +315,11 @@ func newNodeWalker(root *Node) *nodeWalker {
} }
func (nw *nodeWalker) next() { func (nw *nodeWalker) next() {
if (!nw.current.isContainer() || !nw.entering) && nw.current == nw.root { if (!nw.current.IsContainer() || !nw.entering) && nw.current == nw.root {
nw.current = nil nw.current = nil
return return
} }
if nw.entering && nw.current.isContainer() { if nw.entering && nw.current.IsContainer() {
if nw.current.FirstChild != nil { if nw.current.FirstChild != nil {
nw.current = nw.current.FirstChild nw.current = nw.current.FirstChild
nw.entering = true nw.entering = true
......
sudo: false
language: go
go:
- 1.x
- master
matrix:
allow_failures:
- go: master
fast_finish: true
install:
- # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step).
script:
- go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d -s .)
- go tool vet .
- go test -v -race ./...
MIT License
Copyright (c) 2015 Dmitri Shuralyov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
sanitized_anchor_name
=====================
[![Build Status](https://travis-ci.org/shurcooL/sanitized_anchor_name.svg?branch=master)](https://travis-ci.org/shurcooL/sanitized_anchor_name) [![GoDoc](https://godoc.org/github.com/shurcooL/sanitized_anchor_name?status.svg)](https://godoc.org/github.com/shurcooL/sanitized_anchor_name)
Package sanitized_anchor_name provides a func to create sanitized anchor names.
Its logic can be reused by multiple packages to create interoperable anchor names
and links to those anchors.
At this time, it does not try to ensure that generated anchor names
are unique, that responsibility falls on the caller.
Installation
------------
```bash
go get -u github.com/shurcooL/sanitized_anchor_name
```
Example
-------
```Go
anchorName := sanitized_anchor_name.Create("This is a header")
fmt.Println(anchorName)
// Output:
// this-is-a-header
```
License
-------
- [MIT License](LICENSE)
module github.com/shurcooL/sanitized_anchor_name
// Package sanitized_anchor_name provides a func to create sanitized anchor names.
//
// Its logic can be reused by multiple packages to create interoperable anchor names
// and links to those anchors.
//
// At this time, it does not try to ensure that generated anchor names
// are unique, that responsibility falls on the caller.
package sanitized_anchor_name // import "github.com/shurcooL/sanitized_anchor_name"
import "unicode"
// Create returns a sanitized anchor name for the given text.
func Create(text string) string {
var anchorName []rune
var futureDash = false
for _, r := range text {
switch {
case unicode.IsLetter(r) || unicode.IsNumber(r):
if futureDash && len(anchorName) > 0 {
anchorName = append(anchorName, '-')
}
futureDash = false
anchorName = append(anchorName, unicode.ToLower(r))
default:
futureDash = true
}
}
return string(anchorName)
}
...@@ -4,14 +4,12 @@ git: ...@@ -4,14 +4,12 @@ git:
depth: 1 depth: 1
env: env:
- GO111MODULE=on - GO111MODULE=on
go: [1.13.x, 1.14.x] go: 1.15.x
os: [linux, osx] os: linux
install: install:
- ./travis/install.sh - ./travis/install.sh
script: script:
- ./travis/cross_build.sh - cd ci
- ./travis/lint.sh - go run mage.go -v -w ../ crossBuild
- export GOMAXPROCS=4 - go run mage.go -v -w ../ lint
- export GORACE=halt_on_error=1 - go run mage.go -v -w ../ test
- go test -race -v ./...
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then go test -race -v -tags appengine ./... ; fi
# 1.8.1
Code quality:
* move magefile in its own subdir/submodule to remove magefile dependency on logrus consumer
* improve timestamp format documentation
Fixes:
* fix race condition on logger hooks
# 1.8.0
Correct versioning number replacing v1.7.1.
# 1.7.1
Beware this release has introduced a new public API and its semver is therefore incorrect.
Code quality:
* use go 1.15 in travis
* use magefile as task runner
Fixes:
* small fixes about new go 1.13 error formatting system
* Fix for long time race condiction with mutating data hooks
Features:
* build support for zos
# 1.7.0
Fixes:
* the dependency toward a windows terminal library has been removed
Features:
* a new buffer pool management API has been added
* a set of `<LogLevel>Fn()` functions have been added
# 1.6.0 # 1.6.0
Fixes: Fixes:
* end of line cleanup * end of line cleanup
......
...@@ -402,7 +402,7 @@ func (f *MyJSONFormatter) Format(entry *Entry) ([]byte, error) { ...@@ -402,7 +402,7 @@ func (f *MyJSONFormatter) Format(entry *Entry) ([]byte, error) {
// source of the official loggers. // source of the official loggers.
serialized, err := json.Marshal(entry.Data) serialized, err := json.Marshal(entry.Data)
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err) return nil, fmt.Errorf("Failed to marshal fields to JSON, %w", err)
} }
return append(serialized, '\n'), nil return append(serialized, '\n'), nil
} }
......
...@@ -78,6 +78,14 @@ func NewEntry(logger *Logger) *Entry { ...@@ -78,6 +78,14 @@ func NewEntry(logger *Logger) *Entry {
} }
} }
func (entry *Entry) Dup() *Entry {
data := make(Fields, len(entry.Data))
for k, v := range entry.Data {
data[k] = v
}
return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, Context: entry.Context, err: entry.err}
}
// Returns the bytes representation of this entry from the formatter. // Returns the bytes representation of this entry from the formatter.
func (entry *Entry) Bytes() ([]byte, error) { func (entry *Entry) Bytes() ([]byte, error) {
return entry.Logger.Formatter.Format(entry) return entry.Logger.Formatter.Format(entry)
...@@ -123,11 +131,9 @@ func (entry *Entry) WithFields(fields Fields) *Entry { ...@@ -123,11 +131,9 @@ func (entry *Entry) WithFields(fields Fields) *Entry {
for k, v := range fields { for k, v := range fields {
isErrField := false isErrField := false
if t := reflect.TypeOf(v); t != nil { if t := reflect.TypeOf(v); t != nil {
switch t.Kind() { switch {
case reflect.Func: case t.Kind() == reflect.Func, t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Func:
isErrField = true isErrField = true
case reflect.Ptr:
isErrField = t.Elem().Kind() == reflect.Func
} }
} }
if isErrField { if isErrField {
...@@ -212,68 +218,72 @@ func (entry Entry) HasCaller() (has bool) { ...@@ -212,68 +218,72 @@ func (entry Entry) HasCaller() (has bool) {
entry.Caller != nil entry.Caller != nil
} }
// This function is not declared with a pointer value because otherwise func (entry *Entry) log(level Level, msg string) {
// race conditions will occur when using multiple goroutines
func (entry Entry) log(level Level, msg string) {
var buffer *bytes.Buffer var buffer *bytes.Buffer
// Default to now, but allow users to override if they want. newEntry := entry.Dup()
//
// We don't have to worry about polluting future calls to Entry#log() if newEntry.Time.IsZero() {
// with this assignment because this function is declared with a newEntry.Time = time.Now()
// non-pointer receiver.
if entry.Time.IsZero() {
entry.Time = time.Now()
} }
entry.Level = level newEntry.Level = level
entry.Message = msg newEntry.Message = msg
entry.Logger.mu.Lock()
if entry.Logger.ReportCaller { newEntry.Logger.mu.Lock()
entry.Caller = getCaller() reportCaller := newEntry.Logger.ReportCaller
newEntry.Logger.mu.Unlock()
if reportCaller {
newEntry.Caller = getCaller()
} }
entry.Logger.mu.Unlock()
entry.fireHooks() newEntry.fireHooks()
buffer = getBuffer() buffer = getBuffer()
defer func() { defer func() {
entry.Buffer = nil newEntry.Buffer = nil
putBuffer(buffer) putBuffer(buffer)
}() }()
buffer.Reset() buffer.Reset()
entry.Buffer = buffer newEntry.Buffer = buffer
entry.write() newEntry.write()
entry.Buffer = nil newEntry.Buffer = nil
// To avoid Entry#log() returning a value that only would make sense for // To avoid Entry#log() returning a value that only would make sense for
// panic() to use in Entry#Panic(), we avoid the allocation by checking // panic() to use in Entry#Panic(), we avoid the allocation by checking
// directly here. // directly here.
if level <= PanicLevel { if level <= PanicLevel {
panic(&entry) panic(newEntry)
} }
} }
func (entry *Entry) fireHooks() { func (entry *Entry) fireHooks() {
var tmpHooks LevelHooks
entry.Logger.mu.Lock() entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock() tmpHooks = make(LevelHooks, len(entry.Logger.Hooks))
err := entry.Logger.Hooks.Fire(entry.Level, entry) for k, v := range entry.Logger.Hooks {
tmpHooks[k] = v
}
entry.Logger.mu.Unlock()
err := tmpHooks.Fire(entry.Level, entry)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err) fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
} }
} }
func (entry *Entry) write() { func (entry *Entry) write() {
entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock()
serialized, err := entry.Logger.Formatter.Format(entry) serialized, err := entry.Logger.Formatter.Format(entry)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err) fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
return return
} }
if _, err = entry.Logger.Out.Write(serialized); err != nil { entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock()
if _, err := entry.Logger.Out.Write(serialized); err != nil {
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err) fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
} }
} }
...@@ -319,7 +329,6 @@ func (entry *Entry) Fatal(args ...interface{}) { ...@@ -319,7 +329,6 @@ func (entry *Entry) Fatal(args ...interface{}) {
func (entry *Entry) Panic(args ...interface{}) { func (entry *Entry) Panic(args ...interface{}) {
entry.Log(PanicLevel, args...) entry.Log(PanicLevel, args...)
panic(fmt.Sprint(args...))
} }
// Entry Printf family functions // Entry Printf family functions
......
...@@ -4,7 +4,5 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb ...@@ -4,7 +4,5 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
...@@ -23,6 +23,9 @@ func (f FieldMap) resolve(key fieldKey) string { ...@@ -23,6 +23,9 @@ func (f FieldMap) resolve(key fieldKey) string {
// JSONFormatter formats logs into parsable json // JSONFormatter formats logs into parsable json
type JSONFormatter struct { type JSONFormatter struct {
// TimestampFormat sets the format used for marshaling timestamps. // TimestampFormat sets the format used for marshaling timestamps.
// The format to use is the same than for time.Format or time.Parse from the standard
// library.
// The standard Library already provides a set of predefined format.
TimestampFormat string TimestampFormat string
// DisableTimestamp allows disabling automatic timestamps in output // DisableTimestamp allows disabling automatic timestamps in output
...@@ -118,7 +121,7 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { ...@@ -118,7 +121,7 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
encoder.SetIndent("", " ") encoder.SetIndent("", " ")
} }
if err := encoder.Encode(data); err != nil { if err := encoder.Encode(data); err != nil {
return nil, fmt.Errorf("failed to marshal fields to JSON, %v", err) return nil, fmt.Errorf("failed to marshal fields to JSON, %w", err)
} }
return b.Bytes(), nil return b.Bytes(), nil
......
...@@ -12,7 +12,7 @@ import ( ...@@ -12,7 +12,7 @@ import (
// LogFunction For big messages, it can be more efficient to pass a function // LogFunction For big messages, it can be more efficient to pass a function
// and only call it if the log level is actually enables rather than // and only call it if the log level is actually enables rather than
// generating the log message and then checking if the level is enabled // generating the log message and then checking if the level is enabled
type LogFunction func()[]interface{} type LogFunction func() []interface{}
type Logger struct { type Logger struct {
// The logs are `io.Copy`'d to this in a mutex. It's common to set this to a // The logs are `io.Copy`'d to this in a mutex. It's common to set this to a
......
// +build linux aix // +build linux aix zos
// +build !js // +build !js
package logrus package logrus
......
...@@ -53,7 +53,10 @@ type TextFormatter struct { ...@@ -53,7 +53,10 @@ type TextFormatter struct {
// the time passed since beginning of execution. // the time passed since beginning of execution.
FullTimestamp bool FullTimestamp bool
// TimestampFormat to use for display when a full timestamp is printed // TimestampFormat to use for display when a full timestamp is printed.
// The format to use is the same than for time.Format or time.Parse from the standard
// library.
// The standard Library already provides a set of predefined format.
TimestampFormat string TimestampFormat string
// The fields are sorted by default for a consistent output. For applications // The fields are sorted by default for a consistent output. For applications
...@@ -235,6 +238,8 @@ func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []strin ...@@ -235,6 +238,8 @@ func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []strin
levelColor = yellow levelColor = yellow
case ErrorLevel, FatalLevel, PanicLevel: case ErrorLevel, FatalLevel, PanicLevel:
levelColor = red levelColor = red
case InfoLevel:
levelColor = blue
default: default:
levelColor = blue levelColor = blue
} }
......
...@@ -13,12 +13,42 @@ const ( ...@@ -13,12 +13,42 @@ const (
compareGreater compareGreater
) )
var (
intType = reflect.TypeOf(int(1))
int8Type = reflect.TypeOf(int8(1))
int16Type = reflect.TypeOf(int16(1))
int32Type = reflect.TypeOf(int32(1))
int64Type = reflect.TypeOf(int64(1))
uintType = reflect.TypeOf(uint(1))
uint8Type = reflect.TypeOf(uint8(1))
uint16Type = reflect.TypeOf(uint16(1))
uint32Type = reflect.TypeOf(uint32(1))
uint64Type = reflect.TypeOf(uint64(1))
float32Type = reflect.TypeOf(float32(1))
float64Type = reflect.TypeOf(float64(1))
stringType = reflect.TypeOf("")
)
func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
obj1Value := reflect.ValueOf(obj1)
obj2Value := reflect.ValueOf(obj2)
// throughout this switch we try and avoid calling .Convert() if possible,
// as this has a pretty big performance impact
switch kind { switch kind {
case reflect.Int: case reflect.Int:
{ {
intobj1 := obj1.(int) intobj1, ok := obj1.(int)
intobj2 := obj2.(int) if !ok {
intobj1 = obj1Value.Convert(intType).Interface().(int)
}
intobj2, ok := obj2.(int)
if !ok {
intobj2 = obj2Value.Convert(intType).Interface().(int)
}
if intobj1 > intobj2 { if intobj1 > intobj2 {
return compareGreater, true return compareGreater, true
} }
...@@ -31,8 +61,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { ...@@ -31,8 +61,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
} }
case reflect.Int8: case reflect.Int8:
{ {
int8obj1 := obj1.(int8) int8obj1, ok := obj1.(int8)
int8obj2 := obj2.(int8) if !ok {
int8obj1 = obj1Value.Convert(int8Type).Interface().(int8)
}
int8obj2, ok := obj2.(int8)
if !ok {
int8obj2 = obj2Value.Convert(int8Type).Interface().(int8)
}
if int8obj1 > int8obj2 { if int8obj1 > int8obj2 {
return compareGreater, true return compareGreater, true
} }
...@@ -45,8 +81,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { ...@@ -45,8 +81,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
} }
case reflect.Int16: case reflect.Int16:
{ {
int16obj1 := obj1.(int16) int16obj1, ok := obj1.(int16)
int16obj2 := obj2.(int16) if !ok {
int16obj1 = obj1Value.Convert(int16Type).Interface().(int16)
}
int16obj2, ok := obj2.(int16)
if !ok {
int16obj2 = obj2Value.Convert(int16Type).Interface().(int16)
}
if int16obj1 > int16obj2 { if int16obj1 > int16obj2 {
return compareGreater, true return compareGreater, true
} }
...@@ -59,8 +101,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { ...@@ -59,8 +101,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
} }
case reflect.Int32: case reflect.Int32:
{ {
int32obj1 := obj1.(int32) int32obj1, ok := obj1.(int32)
int32obj2 := obj2.(int32) if !ok {
int32obj1 = obj1Value.Convert(int32Type).Interface().(int32)
}
int32obj2, ok := obj2.(int32)
if !ok {
int32obj2 = obj2Value.Convert(int32Type).Interface().(int32)
}
if int32obj1 > int32obj2 { if int32obj1 > int32obj2 {
return compareGreater, true return compareGreater, true
} }
...@@ -73,8 +121,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { ...@@ -73,8 +121,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
} }
case reflect.Int64: case reflect.Int64:
{ {
int64obj1 := obj1.(int64) int64obj1, ok := obj1.(int64)
int64obj2 := obj2.(int64) if !ok {
int64obj1 = obj1Value.Convert(int64Type).Interface().(int64)
}
int64obj2, ok := obj2.(int64)
if !ok {
int64obj2 = obj2Value.Convert(int64Type).Interface().(int64)
}
if int64obj1 > int64obj2 { if int64obj1 > int64obj2 {
return compareGreater, true return compareGreater, true
} }
...@@ -87,8 +141,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { ...@@ -87,8 +141,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
} }
case reflect.Uint: case reflect.Uint:
{ {
uintobj1 := obj1.(uint) uintobj1, ok := obj1.(uint)
uintobj2 := obj2.(uint) if !ok {
uintobj1 = obj1Value.Convert(uintType).Interface().(uint)
}
uintobj2, ok := obj2.(uint)
if !ok {
uintobj2 = obj2Value.Convert(uintType).Interface().(uint)
}
if uintobj1 > uintobj2 { if uintobj1 > uintobj2 {
return compareGreater, true return compareGreater, true
} }
...@@ -101,8 +161,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { ...@@ -101,8 +161,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
} }
case reflect.Uint8: case reflect.Uint8:
{ {
uint8obj1 := obj1.(uint8) uint8obj1, ok := obj1.(uint8)
uint8obj2 := obj2.(uint8) if !ok {
uint8obj1 = obj1Value.Convert(uint8Type).Interface().(uint8)
}
uint8obj2, ok := obj2.(uint8)
if !ok {
uint8obj2 = obj2Value.Convert(uint8Type).Interface().(uint8)
}
if uint8obj1 > uint8obj2 { if uint8obj1 > uint8obj2 {
return compareGreater, true return compareGreater, true
} }
...@@ -115,8 +181,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { ...@@ -115,8 +181,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
} }
case reflect.Uint16: case reflect.Uint16:
{ {
uint16obj1 := obj1.(uint16) uint16obj1, ok := obj1.(uint16)
uint16obj2 := obj2.(uint16) if !ok {
uint16obj1 = obj1Value.Convert(uint16Type).Interface().(uint16)
}
uint16obj2, ok := obj2.(uint16)
if !ok {
uint16obj2 = obj2Value.Convert(uint16Type).Interface().(uint16)
}
if uint16obj1 > uint16obj2 { if uint16obj1 > uint16obj2 {
return compareGreater, true return compareGreater, true
} }
...@@ -129,8 +201,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { ...@@ -129,8 +201,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
} }
case reflect.Uint32: case reflect.Uint32:
{ {
uint32obj1 := obj1.(uint32) uint32obj1, ok := obj1.(uint32)
uint32obj2 := obj2.(uint32) if !ok {
uint32obj1 = obj1Value.Convert(uint32Type).Interface().(uint32)
}
uint32obj2, ok := obj2.(uint32)
if !ok {
uint32obj2 = obj2Value.Convert(uint32Type).Interface().(uint32)
}
if uint32obj1 > uint32obj2 { if uint32obj1 > uint32obj2 {
return compareGreater, true return compareGreater, true
} }
...@@ -143,8 +221,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { ...@@ -143,8 +221,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
} }
case reflect.Uint64: case reflect.Uint64:
{ {
uint64obj1 := obj1.(uint64) uint64obj1, ok := obj1.(uint64)
uint64obj2 := obj2.(uint64) if !ok {
uint64obj1 = obj1Value.Convert(uint64Type).Interface().(uint64)
}
uint64obj2, ok := obj2.(uint64)
if !ok {
uint64obj2 = obj2Value.Convert(uint64Type).Interface().(uint64)
}
if uint64obj1 > uint64obj2 { if uint64obj1 > uint64obj2 {
return compareGreater, true return compareGreater, true
} }
...@@ -157,8 +241,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { ...@@ -157,8 +241,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
} }
case reflect.Float32: case reflect.Float32:
{ {
float32obj1 := obj1.(float32) float32obj1, ok := obj1.(float32)
float32obj2 := obj2.(float32) if !ok {
float32obj1 = obj1Value.Convert(float32Type).Interface().(float32)
}
float32obj2, ok := obj2.(float32)
if !ok {
float32obj2 = obj2Value.Convert(float32Type).Interface().(float32)
}
if float32obj1 > float32obj2 { if float32obj1 > float32obj2 {
return compareGreater, true return compareGreater, true
} }
...@@ -171,8 +261,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { ...@@ -171,8 +261,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
} }
case reflect.Float64: case reflect.Float64:
{ {
float64obj1 := obj1.(float64) float64obj1, ok := obj1.(float64)
float64obj2 := obj2.(float64) if !ok {
float64obj1 = obj1Value.Convert(float64Type).Interface().(float64)
}
float64obj2, ok := obj2.(float64)
if !ok {
float64obj2 = obj2Value.Convert(float64Type).Interface().(float64)
}
if float64obj1 > float64obj2 { if float64obj1 > float64obj2 {
return compareGreater, true return compareGreater, true
} }
...@@ -185,8 +281,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { ...@@ -185,8 +281,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
} }
case reflect.String: case reflect.String:
{ {
stringobj1 := obj1.(string) stringobj1, ok := obj1.(string)
stringobj2 := obj2.(string) if !ok {
stringobj1 = obj1Value.Convert(stringType).Interface().(string)
}
stringobj2, ok := obj2.(string)
if !ok {
stringobj2 = obj2Value.Convert(stringType).Interface().(string)
}
if stringobj1 > stringobj2 { if stringobj1 > stringobj2 {
return compareGreater, true return compareGreater, true
} }
...@@ -240,6 +342,24 @@ func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...inter ...@@ -240,6 +342,24 @@ func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...inter
return compareTwoValues(t, e1, e2, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs) return compareTwoValues(t, e1, e2, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs)
} }
// Positive asserts that the specified element is positive
//
// assert.Positive(t, 1)
// assert.Positive(t, 1.23)
func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool {
zero := reflect.Zero(reflect.TypeOf(e))
return compareTwoValues(t, e, zero.Interface(), []CompareType{compareGreater}, "\"%v\" is not positive", msgAndArgs)
}
// Negative asserts that the specified element is negative
//
// assert.Negative(t, -1)
// assert.Negative(t, -1.23)
func Negative(t TestingT, e interface{}, msgAndArgs ...interface{}) bool {
zero := reflect.Zero(reflect.TypeOf(e))
return compareTwoValues(t, e, zero.Interface(), []CompareType{compareLess}, "\"%v\" is not negative", msgAndArgs)
}
func compareTwoValues(t TestingT, e1 interface{}, e2 interface{}, allowedComparesResults []CompareType, failMessage string, msgAndArgs ...interface{}) bool { func compareTwoValues(t TestingT, e1 interface{}, e2 interface{}, allowedComparesResults []CompareType, failMessage string, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok { if h, ok := t.(tHelper); ok {
h.Helper() h.Helper()
......
...@@ -114,6 +114,24 @@ func Errorf(t TestingT, err error, msg string, args ...interface{}) bool { ...@@ -114,6 +114,24 @@ func Errorf(t TestingT, err error, msg string, args ...interface{}) bool {
return Error(t, err, append([]interface{}{msg}, args...)...) return Error(t, err, append([]interface{}{msg}, args...)...)
} }
// ErrorAsf asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value.
// This is a wrapper for errors.As.
func ErrorAsf(t TestingT, err error, target interface{}, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return ErrorAs(t, err, target, append([]interface{}{msg}, args...)...)
}
// ErrorIsf asserts that at least one of the errors in err's chain matches target.
// This is a wrapper for errors.Is.
func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return ErrorIs(t, err, target, append([]interface{}{msg}, args...)...)
}
// Eventuallyf asserts that given condition will be met in waitFor time, // Eventuallyf asserts that given condition will be met in waitFor time,
// periodically checking target function each tick. // periodically checking target function each tick.
// //
...@@ -321,6 +339,54 @@ func InEpsilonSlicef(t TestingT, expected interface{}, actual interface{}, epsil ...@@ -321,6 +339,54 @@ func InEpsilonSlicef(t TestingT, expected interface{}, actual interface{}, epsil
return InEpsilonSlice(t, expected, actual, epsilon, append([]interface{}{msg}, args...)...) return InEpsilonSlice(t, expected, actual, epsilon, append([]interface{}{msg}, args...)...)
} }
// IsDecreasingf asserts that the collection is decreasing
//
// assert.IsDecreasingf(t, []int{2, 1, 0}, "error message %s", "formatted")
// assert.IsDecreasingf(t, []float{2, 1}, "error message %s", "formatted")
// assert.IsDecreasingf(t, []string{"b", "a"}, "error message %s", "formatted")
func IsDecreasingf(t TestingT, object interface{}, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return IsDecreasing(t, object, append([]interface{}{msg}, args...)...)
}
// IsIncreasingf asserts that the collection is increasing
//
// assert.IsIncreasingf(t, []int{1, 2, 3}, "error message %s", "formatted")
// assert.IsIncreasingf(t, []float{1, 2}, "error message %s", "formatted")
// assert.IsIncreasingf(t, []string{"a", "b"}, "error message %s", "formatted")
func IsIncreasingf(t TestingT, object interface{}, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return IsIncreasing(t, object, append([]interface{}{msg}, args...)...)
}
// IsNonDecreasingf asserts that the collection is not decreasing
//
// assert.IsNonDecreasingf(t, []int{1, 1, 2}, "error message %s", "formatted")
// assert.IsNonDecreasingf(t, []float{1, 2}, "error message %s", "formatted")
// assert.IsNonDecreasingf(t, []string{"a", "b"}, "error message %s", "formatted")
func IsNonDecreasingf(t TestingT, object interface{}, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return IsNonDecreasing(t, object, append([]interface{}{msg}, args...)...)
}
// IsNonIncreasingf asserts that the collection is not increasing
//
// assert.IsNonIncreasingf(t, []int{2, 1, 1}, "error message %s", "formatted")
// assert.IsNonIncreasingf(t, []float{2, 1}, "error message %s", "formatted")
// assert.IsNonIncreasingf(t, []string{"b", "a"}, "error message %s", "formatted")
func IsNonIncreasingf(t TestingT, object interface{}, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return IsNonIncreasing(t, object, append([]interface{}{msg}, args...)...)
}
// IsTypef asserts that the specified objects are of the same type. // IsTypef asserts that the specified objects are of the same type.
func IsTypef(t TestingT, expectedType interface{}, object interface{}, msg string, args ...interface{}) bool { func IsTypef(t TestingT, expectedType interface{}, object interface{}, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok { if h, ok := t.(tHelper); ok {
...@@ -375,6 +441,17 @@ func LessOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args . ...@@ -375,6 +441,17 @@ func LessOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args .
return LessOrEqual(t, e1, e2, append([]interface{}{msg}, args...)...) return LessOrEqual(t, e1, e2, append([]interface{}{msg}, args...)...)
} }
// Negativef asserts that the specified element is negative
//
// assert.Negativef(t, -1, "error message %s", "formatted")
// assert.Negativef(t, -1.23, "error message %s", "formatted")
func Negativef(t TestingT, e interface{}, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return Negative(t, e, append([]interface{}{msg}, args...)...)
}
// Neverf asserts that the given condition doesn't satisfy in waitFor time, // Neverf asserts that the given condition doesn't satisfy in waitFor time,
// periodically checking the target function each tick. // periodically checking the target function each tick.
// //
...@@ -476,6 +553,15 @@ func NotEqualValuesf(t TestingT, expected interface{}, actual interface{}, msg s ...@@ -476,6 +553,15 @@ func NotEqualValuesf(t TestingT, expected interface{}, actual interface{}, msg s
return NotEqualValues(t, expected, actual, append([]interface{}{msg}, args...)...) return NotEqualValues(t, expected, actual, append([]interface{}{msg}, args...)...)
} }
// NotErrorIsf asserts that at none of the errors in err's chain matches target.
// This is a wrapper for errors.Is.
func NotErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return NotErrorIs(t, err, target, append([]interface{}{msg}, args...)...)
}
// NotNilf asserts that the specified object is not nil. // NotNilf asserts that the specified object is not nil.
// //
// assert.NotNilf(t, err, "error message %s", "formatted") // assert.NotNilf(t, err, "error message %s", "formatted")
...@@ -572,6 +658,17 @@ func PanicsWithValuef(t TestingT, expected interface{}, f PanicTestFunc, msg str ...@@ -572,6 +658,17 @@ func PanicsWithValuef(t TestingT, expected interface{}, f PanicTestFunc, msg str
return PanicsWithValue(t, expected, f, append([]interface{}{msg}, args...)...) return PanicsWithValue(t, expected, f, append([]interface{}{msg}, args...)...)
} }
// Positivef asserts that the specified element is positive
//
// assert.Positivef(t, 1, "error message %s", "formatted")
// assert.Positivef(t, 1.23, "error message %s", "formatted")
func Positivef(t TestingT, e interface{}, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return Positive(t, e, append([]interface{}{msg}, args...)...)
}
// Regexpf asserts that a specified regexp matches a string. // Regexpf asserts that a specified regexp matches a string.
// //
// assert.Regexpf(t, regexp.MustCompile("start"), "it's starting", "error message %s", "formatted") // assert.Regexpf(t, regexp.MustCompile("start"), "it's starting", "error message %s", "formatted")
......
...@@ -204,6 +204,42 @@ func (a *Assertions) Error(err error, msgAndArgs ...interface{}) bool { ...@@ -204,6 +204,42 @@ func (a *Assertions) Error(err error, msgAndArgs ...interface{}) bool {
return Error(a.t, err, msgAndArgs...) return Error(a.t, err, msgAndArgs...)
} }
// ErrorAs asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value.
// This is a wrapper for errors.As.
func (a *Assertions) ErrorAs(err error, target interface{}, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return ErrorAs(a.t, err, target, msgAndArgs...)
}
// ErrorAsf asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value.
// This is a wrapper for errors.As.
func (a *Assertions) ErrorAsf(err error, target interface{}, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return ErrorAsf(a.t, err, target, msg, args...)
}
// ErrorIs asserts that at least one of the errors in err's chain matches target.
// This is a wrapper for errors.Is.
func (a *Assertions) ErrorIs(err error, target error, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return ErrorIs(a.t, err, target, msgAndArgs...)
}
// ErrorIsf asserts that at least one of the errors in err's chain matches target.
// This is a wrapper for errors.Is.
func (a *Assertions) ErrorIsf(err error, target error, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return ErrorIsf(a.t, err, target, msg, args...)
}
// Errorf asserts that a function returned an error (i.e. not `nil`). // Errorf asserts that a function returned an error (i.e. not `nil`).
// //
// actualObj, err := SomeFunction() // actualObj, err := SomeFunction()
...@@ -631,6 +667,102 @@ func (a *Assertions) InEpsilonf(expected interface{}, actual interface{}, epsilo ...@@ -631,6 +667,102 @@ func (a *Assertions) InEpsilonf(expected interface{}, actual interface{}, epsilo
return InEpsilonf(a.t, expected, actual, epsilon, msg, args...) return InEpsilonf(a.t, expected, actual, epsilon, msg, args...)
} }
// IsDecreasing asserts that the collection is decreasing
//
// a.IsDecreasing([]int{2, 1, 0})
// a.IsDecreasing([]float{2, 1})
// a.IsDecreasing([]string{"b", "a"})
func (a *Assertions) IsDecreasing(object interface{}, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return IsDecreasing(a.t, object, msgAndArgs...)
}
// IsDecreasingf asserts that the collection is decreasing
//
// a.IsDecreasingf([]int{2, 1, 0}, "error message %s", "formatted")
// a.IsDecreasingf([]float{2, 1}, "error message %s", "formatted")
// a.IsDecreasingf([]string{"b", "a"}, "error message %s", "formatted")
func (a *Assertions) IsDecreasingf(object interface{}, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return IsDecreasingf(a.t, object, msg, args...)
}
// IsIncreasing asserts that the collection is increasing
//
// a.IsIncreasing([]int{1, 2, 3})
// a.IsIncreasing([]float{1, 2})
// a.IsIncreasing([]string{"a", "b"})
func (a *Assertions) IsIncreasing(object interface{}, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return IsIncreasing(a.t, object, msgAndArgs...)
}
// IsIncreasingf asserts that the collection is increasing
//
// a.IsIncreasingf([]int{1, 2, 3}, "error message %s", "formatted")
// a.IsIncreasingf([]float{1, 2}, "error message %s", "formatted")
// a.IsIncreasingf([]string{"a", "b"}, "error message %s", "formatted")
func (a *Assertions) IsIncreasingf(object interface{}, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return IsIncreasingf(a.t, object, msg, args...)
}
// IsNonDecreasing asserts that the collection is not decreasing
//
// a.IsNonDecreasing([]int{1, 1, 2})
// a.IsNonDecreasing([]float{1, 2})
// a.IsNonDecreasing([]string{"a", "b"})
func (a *Assertions) IsNonDecreasing(object interface{}, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return IsNonDecreasing(a.t, object, msgAndArgs...)
}
// IsNonDecreasingf asserts that the collection is not decreasing
//
// a.IsNonDecreasingf([]int{1, 1, 2}, "error message %s", "formatted")
// a.IsNonDecreasingf([]float{1, 2}, "error message %s", "formatted")
// a.IsNonDecreasingf([]string{"a", "b"}, "error message %s", "formatted")
func (a *Assertions) IsNonDecreasingf(object interface{}, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return IsNonDecreasingf(a.t, object, msg, args...)
}
// IsNonIncreasing asserts that the collection is not increasing
//
// a.IsNonIncreasing([]int{2, 1, 1})
// a.IsNonIncreasing([]float{2, 1})
// a.IsNonIncreasing([]string{"b", "a"})
func (a *Assertions) IsNonIncreasing(object interface{}, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return IsNonIncreasing(a.t, object, msgAndArgs...)
}
// IsNonIncreasingf asserts that the collection is not increasing
//
// a.IsNonIncreasingf([]int{2, 1, 1}, "error message %s", "formatted")
// a.IsNonIncreasingf([]float{2, 1}, "error message %s", "formatted")
// a.IsNonIncreasingf([]string{"b", "a"}, "error message %s", "formatted")
func (a *Assertions) IsNonIncreasingf(object interface{}, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return IsNonIncreasingf(a.t, object, msg, args...)
}
// IsType asserts that the specified objects are of the same type. // IsType asserts that the specified objects are of the same type.
func (a *Assertions) IsType(expectedType interface{}, object interface{}, msgAndArgs ...interface{}) bool { func (a *Assertions) IsType(expectedType interface{}, object interface{}, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok { if h, ok := a.t.(tHelper); ok {
...@@ -739,6 +871,28 @@ func (a *Assertions) Lessf(e1 interface{}, e2 interface{}, msg string, args ...i ...@@ -739,6 +871,28 @@ func (a *Assertions) Lessf(e1 interface{}, e2 interface{}, msg string, args ...i
return Lessf(a.t, e1, e2, msg, args...) return Lessf(a.t, e1, e2, msg, args...)
} }
// Negative asserts that the specified element is negative
//
// a.Negative(-1)
// a.Negative(-1.23)
func (a *Assertions) Negative(e interface{}, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return Negative(a.t, e, msgAndArgs...)
}
// Negativef asserts that the specified element is negative
//
// a.Negativef(-1, "error message %s", "formatted")
// a.Negativef(-1.23, "error message %s", "formatted")
func (a *Assertions) Negativef(e interface{}, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return Negativef(a.t, e, msg, args...)
}
// Never asserts that the given condition doesn't satisfy in waitFor time, // Never asserts that the given condition doesn't satisfy in waitFor time,
// periodically checking the target function each tick. // periodically checking the target function each tick.
// //
...@@ -941,6 +1095,24 @@ func (a *Assertions) NotEqualf(expected interface{}, actual interface{}, msg str ...@@ -941,6 +1095,24 @@ func (a *Assertions) NotEqualf(expected interface{}, actual interface{}, msg str
return NotEqualf(a.t, expected, actual, msg, args...) return NotEqualf(a.t, expected, actual, msg, args...)
} }
// NotErrorIs asserts that at none of the errors in err's chain matches target.
// This is a wrapper for errors.Is.
func (a *Assertions) NotErrorIs(err error, target error, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return NotErrorIs(a.t, err, target, msgAndArgs...)
}
// NotErrorIsf asserts that at none of the errors in err's chain matches target.
// This is a wrapper for errors.Is.
func (a *Assertions) NotErrorIsf(err error, target error, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return NotErrorIsf(a.t, err, target, msg, args...)
}
// NotNil asserts that the specified object is not nil. // NotNil asserts that the specified object is not nil.
// //
// a.NotNil(err) // a.NotNil(err)
...@@ -1133,6 +1305,28 @@ func (a *Assertions) Panicsf(f PanicTestFunc, msg string, args ...interface{}) b ...@@ -1133,6 +1305,28 @@ func (a *Assertions) Panicsf(f PanicTestFunc, msg string, args ...interface{}) b
return Panicsf(a.t, f, msg, args...) return Panicsf(a.t, f, msg, args...)
} }
// Positive asserts that the specified element is positive
//
// a.Positive(1)
// a.Positive(1.23)
func (a *Assertions) Positive(e interface{}, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return Positive(a.t, e, msgAndArgs...)
}
// Positivef asserts that the specified element is positive
//
// a.Positivef(1, "error message %s", "formatted")
// a.Positivef(1.23, "error message %s", "formatted")
func (a *Assertions) Positivef(e interface{}, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return Positivef(a.t, e, msg, args...)
}
// Regexp asserts that a specified regexp matches a string. // Regexp asserts that a specified regexp matches a string.
// //
// a.Regexp(regexp.MustCompile("start"), "it's starting") // a.Regexp(regexp.MustCompile("start"), "it's starting")
......
package assert
import (
"fmt"
"reflect"
)
// isOrdered checks that collection contains orderable elements.
func isOrdered(t TestingT, object interface{}, allowedComparesResults []CompareType, failMessage string, msgAndArgs ...interface{}) bool {
objKind := reflect.TypeOf(object).Kind()
if objKind != reflect.Slice && objKind != reflect.Array {
return false
}
objValue := reflect.ValueOf(object)
objLen := objValue.Len()
if objLen <= 1 {
return true
}
value := objValue.Index(0)
valueInterface := value.Interface()
firstValueKind := value.Kind()
for i := 1; i < objLen; i++ {
prevValue := value
prevValueInterface := valueInterface
value = objValue.Index(i)
valueInterface = value.Interface()
compareResult, isComparable := compare(prevValueInterface, valueInterface, firstValueKind)
if !isComparable {
return Fail(t, fmt.Sprintf("Can not compare type \"%s\" and \"%s\"", reflect.TypeOf(value), reflect.TypeOf(prevValue)), msgAndArgs...)
}
if !containsValue(allowedComparesResults, compareResult) {
return Fail(t, fmt.Sprintf(failMessage, prevValue, value), msgAndArgs...)
}
}
return true
}
// IsIncreasing asserts that the collection is increasing
//
// assert.IsIncreasing(t, []int{1, 2, 3})
// assert.IsIncreasing(t, []float{1, 2})
// assert.IsIncreasing(t, []string{"a", "b"})
func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
return isOrdered(t, object, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs)
}
// IsNonIncreasing asserts that the collection is not increasing
//
// assert.IsNonIncreasing(t, []int{2, 1, 1})
// assert.IsNonIncreasing(t, []float{2, 1})
// assert.IsNonIncreasing(t, []string{"b", "a"})
func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
return isOrdered(t, object, []CompareType{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs)
}
// IsDecreasing asserts that the collection is decreasing
//
// assert.IsDecreasing(t, []int{2, 1, 0})
// assert.IsDecreasing(t, []float{2, 1})
// assert.IsDecreasing(t, []string{"b", "a"})
func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
return isOrdered(t, object, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs)
}
// IsNonDecreasing asserts that the collection is not decreasing
//
// assert.IsNonDecreasing(t, []int{1, 1, 2})
// assert.IsNonDecreasing(t, []float{1, 2})
// assert.IsNonDecreasing(t, []string{"a", "b"})
func IsNonDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
return isOrdered(t, object, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs)
}
...@@ -172,8 +172,8 @@ func isTest(name, prefix string) bool { ...@@ -172,8 +172,8 @@ func isTest(name, prefix string) bool {
if len(name) == len(prefix) { // "Test" is ok if len(name) == len(prefix) { // "Test" is ok
return true return true
} }
rune, _ := utf8.DecodeRuneInString(name[len(prefix):]) r, _ := utf8.DecodeRuneInString(name[len(prefix):])
return !unicode.IsLower(rune) return !unicode.IsLower(r)
} }
func messageFromMsgAndArgs(msgAndArgs ...interface{}) string { func messageFromMsgAndArgs(msgAndArgs ...interface{}) string {
...@@ -1622,6 +1622,7 @@ var spewConfig = spew.ConfigState{ ...@@ -1622,6 +1622,7 @@ var spewConfig = spew.ConfigState{
DisableCapacities: true, DisableCapacities: true,
SortKeys: true, SortKeys: true,
DisableMethods: true, DisableMethods: true,
MaxDepth: 10,
} }
type tHelper interface { type tHelper interface {
...@@ -1693,3 +1694,81 @@ func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.D ...@@ -1693,3 +1694,81 @@ func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.D
} }
} }
} }
// ErrorIs asserts that at least one of the errors in err's chain matches target.
// This is a wrapper for errors.Is.
func ErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
if errors.Is(err, target) {
return true
}
var expectedText string
if target != nil {
expectedText = target.Error()
}
chain := buildErrorChainString(err)
return Fail(t, fmt.Sprintf("Target error should be in err chain:\n"+
"expected: %q\n"+
"in chain: %s", expectedText, chain,
), msgAndArgs...)
}
// NotErrorIs asserts that at none of the errors in err's chain matches target.
// This is a wrapper for errors.Is.
func NotErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
if !errors.Is(err, target) {
return true
}
var expectedText string
if target != nil {
expectedText = target.Error()
}
chain := buildErrorChainString(err)
return Fail(t, fmt.Sprintf("Target error should not be in err chain:\n"+
"found: %q\n"+
"in chain: %s", expectedText, chain,
), msgAndArgs...)
}
// ErrorAs asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value.
// This is a wrapper for errors.As.
func ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
if errors.As(err, target) {
return true
}
chain := buildErrorChainString(err)
return Fail(t, fmt.Sprintf("Should be in error chain:\n"+
"expected: %q\n"+
"in chain: %s", target, chain,
), msgAndArgs...)
}
func buildErrorChainString(err error) string {
if err == nil {
return ""
}
e := errors.Unwrap(err)
chain := fmt.Sprintf("%q", err.Error())
for e != nil {
chain += fmt.Sprintf("\n\t%q", e.Error())
e = errors.Unwrap(e)
}
return chain
}
...@@ -524,7 +524,7 @@ github.com/go-openapi/swag ...@@ -524,7 +524,7 @@ github.com/go-openapi/swag
github.com/go-sql-driver/mysql github.com/go-sql-driver/mysql
# github.com/godbus/dbus/v5 v5.0.4 # github.com/godbus/dbus/v5 v5.0.4
github.com/godbus/dbus/v5 github.com/godbus/dbus/v5
# github.com/gofrs/flock v0.7.1 # github.com/gofrs/flock v0.8.1
github.com/gofrs/flock github.com/gofrs/flock
# github.com/gogo/googleapis v1.4.0 # github.com/gogo/googleapis v1.4.0
github.com/gogo/googleapis/google/rpc github.com/gogo/googleapis/google/rpc
...@@ -608,7 +608,7 @@ github.com/google/gofuzz ...@@ -608,7 +608,7 @@ github.com/google/gofuzz
# github.com/google/tcpproxy v0.0.0-20180808230851-dfa16c61dad2 # github.com/google/tcpproxy v0.0.0-20180808230851-dfa16c61dad2
## explicit ## explicit
github.com/google/tcpproxy github.com/google/tcpproxy
# github.com/google/uuid v1.2.0 # github.com/google/uuid v1.3.0
## explicit ## explicit
github.com/google/uuid github.com/google/uuid
# github.com/googleapis/gax-go/v2 v2.0.5 # github.com/googleapis/gax-go/v2 v2.0.5
...@@ -781,7 +781,7 @@ github.com/mitchellh/go-wordwrap ...@@ -781,7 +781,7 @@ github.com/mitchellh/go-wordwrap
github.com/mitchellh/mapstructure github.com/mitchellh/mapstructure
# github.com/moby/ipvs v1.0.1 # github.com/moby/ipvs v1.0.1
github.com/moby/ipvs github.com/moby/ipvs
# github.com/moby/sys/mountinfo v0.1.3 # github.com/moby/sys/mountinfo v0.4.1 => github.com/moby/sys/mountinfo v0.1.3
github.com/moby/sys/mountinfo github.com/moby/sys/mountinfo
# github.com/moby/term v0.0.0-20200312100748-672ec06f55cd # github.com/moby/term v0.0.0-20200312100748-672ec06f55cd
github.com/moby/term github.com/moby/term
...@@ -931,7 +931,7 @@ github.com/robfig/cron ...@@ -931,7 +931,7 @@ github.com/robfig/cron
# github.com/robfig/cron/v3 v3.0.1 # github.com/robfig/cron/v3 v3.0.1
## explicit ## explicit
github.com/robfig/cron/v3 github.com/robfig/cron/v3
# github.com/rootless-containers/rootlesskit v0.10.0 # github.com/rootless-containers/rootlesskit v0.14.5
## explicit ## explicit
github.com/rootless-containers/rootlesskit/pkg/api github.com/rootless-containers/rootlesskit/pkg/api
github.com/rootless-containers/rootlesskit/pkg/api/client github.com/rootless-containers/rootlesskit/pkg/api/client
...@@ -946,6 +946,7 @@ github.com/rootless-containers/rootlesskit/pkg/network/iputils ...@@ -946,6 +946,7 @@ github.com/rootless-containers/rootlesskit/pkg/network/iputils
github.com/rootless-containers/rootlesskit/pkg/network/parentutils github.com/rootless-containers/rootlesskit/pkg/network/parentutils
github.com/rootless-containers/rootlesskit/pkg/network/slirp4netns github.com/rootless-containers/rootlesskit/pkg/network/slirp4netns
github.com/rootless-containers/rootlesskit/pkg/parent github.com/rootless-containers/rootlesskit/pkg/parent
github.com/rootless-containers/rootlesskit/pkg/parent/cgrouputil
github.com/rootless-containers/rootlesskit/pkg/parent/idtools github.com/rootless-containers/rootlesskit/pkg/parent/idtools
github.com/rootless-containers/rootlesskit/pkg/port github.com/rootless-containers/rootlesskit/pkg/port
github.com/rootless-containers/rootlesskit/pkg/port/builtin github.com/rootless-containers/rootlesskit/pkg/port/builtin
...@@ -959,21 +960,20 @@ github.com/rootless-containers/rootlesskit/pkg/port/builtin/parent/udp/udpproxy ...@@ -959,21 +960,20 @@ github.com/rootless-containers/rootlesskit/pkg/port/builtin/parent/udp/udpproxy
github.com/rootless-containers/rootlesskit/pkg/port/portutil github.com/rootless-containers/rootlesskit/pkg/port/portutil
github.com/rootless-containers/rootlesskit/pkg/sigproxy github.com/rootless-containers/rootlesskit/pkg/sigproxy
github.com/rootless-containers/rootlesskit/pkg/sigproxy/signal github.com/rootless-containers/rootlesskit/pkg/sigproxy/signal
github.com/rootless-containers/rootlesskit/pkg/version
# github.com/rs/xid v1.2.1 # github.com/rs/xid v1.2.1
github.com/rs/xid github.com/rs/xid
# github.com/rubiojr/go-vhd v0.0.0-20200706105327-02e210299021 # github.com/rubiojr/go-vhd v0.0.0-20200706105327-02e210299021
github.com/rubiojr/go-vhd/vhd github.com/rubiojr/go-vhd/vhd
# github.com/russross/blackfriday v1.5.2 # github.com/russross/blackfriday v1.5.2
github.com/russross/blackfriday github.com/russross/blackfriday
# github.com/russross/blackfriday/v2 v2.0.1 # github.com/russross/blackfriday/v2 v2.1.0
github.com/russross/blackfriday/v2 github.com/russross/blackfriday/v2
# github.com/satori/go.uuid v1.2.0 # github.com/satori/go.uuid v1.2.0
github.com/satori/go.uuid github.com/satori/go.uuid
# github.com/seccomp/libseccomp-golang v0.9.1 # github.com/seccomp/libseccomp-golang v0.9.1
github.com/seccomp/libseccomp-golang github.com/seccomp/libseccomp-golang
# github.com/shurcooL/sanitized_anchor_name v1.0.0 # github.com/sirupsen/logrus v1.8.1
github.com/shurcooL/sanitized_anchor_name
# github.com/sirupsen/logrus v1.7.0
## explicit ## explicit
github.com/sirupsen/logrus github.com/sirupsen/logrus
# github.com/soheilhy/cmux v0.1.4 # github.com/soheilhy/cmux v0.1.4
...@@ -988,7 +988,7 @@ github.com/spf13/cobra ...@@ -988,7 +988,7 @@ github.com/spf13/cobra
github.com/spf13/pflag github.com/spf13/pflag
# github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 # github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980
github.com/stefanberger/go-pkcs11uri github.com/stefanberger/go-pkcs11uri
# github.com/stretchr/testify v1.6.1 # github.com/stretchr/testify v1.7.0
## explicit ## explicit
github.com/stretchr/testify/assert github.com/stretchr/testify/assert
# github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 # github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2
...@@ -3036,6 +3036,7 @@ sigs.k8s.io/yaml ...@@ -3036,6 +3036,7 @@ sigs.k8s.io/yaml
# github.com/juju/errors => github.com/k3s-io/nocode v0.0.0-20200630202308-cb097102c09f # github.com/juju/errors => github.com/k3s-io/nocode v0.0.0-20200630202308-cb097102c09f
# github.com/kubernetes-sigs/cri-tools => github.com/k3s-io/cri-tools v1.20.0-k3s1 # github.com/kubernetes-sigs/cri-tools => github.com/k3s-io/cri-tools v1.20.0-k3s1
# github.com/matryer/moq => github.com/rancher/moq v0.0.0-20190404221404-ee5226d43009 # github.com/matryer/moq => github.com/rancher/moq v0.0.0-20190404221404-ee5226d43009
# github.com/moby/sys/mountinfo => github.com/moby/sys/mountinfo v0.1.3
# github.com/opencontainers/runc => github.com/opencontainers/runc v1.0.0-rc92 # github.com/opencontainers/runc => github.com/opencontainers/runc v1.0.0-rc92
# github.com/opencontainers/runtime-spec => github.com/opencontainers/runtime-spec v1.0.3-0.20200728170252-4d89ac9fbff6 # github.com/opencontainers/runtime-spec => github.com/opencontainers/runtime-spec v1.0.3-0.20200728170252-4d89ac9fbff6
# github.com/rancher/k3s/pkg/data => ./pkg/data # github.com/rancher/k3s/pkg/data => ./pkg/data
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment