mirror of https://github.com/containers/conmon.git
Compare commits
90 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
92e29d9b78 | |
|
|
84edf228f5 | |
|
|
4566e2ff8f | |
|
|
11bcc31eae | |
|
|
b91d4ee0f7 | |
|
|
61dee88fcd | |
|
|
270aa5a640 | |
|
|
05882adca0 | |
|
|
ba1eadff5d | |
|
|
83cfddd484 | |
|
|
3b997548f7 | |
|
|
74bd7ce836 | |
|
|
ef7009e57f | |
|
|
d92af7adae | |
|
|
90d60381b9 | |
|
|
a39e92e74c | |
|
|
7d82b28a6b | |
|
|
238b1d099c | |
|
|
29d17becab | |
|
|
c5c98fd510 | |
|
|
c926ba0bf1 | |
|
|
724e771056 | |
|
|
e6609cebfe | |
|
|
f690e02cae | |
|
|
b2f13b0f12 | |
|
|
2a1dda8e92 | |
|
|
4d374fd67d | |
|
|
ede56b9af5 | |
|
|
9b74c3258c | |
|
|
f37e9e795c | |
|
|
de270e6eb9 | |
|
|
e86e03feab | |
|
|
ecf16be6c3 | |
|
|
93dcd632c5 | |
|
|
c85e7bb4ed | |
|
|
0a5e93d95b | |
|
|
0b024b29da | |
|
|
7c7b0c5bbc | |
|
|
12c3a59925 | |
|
|
5b51069234 | |
|
|
58e4cf40fc | |
|
|
9389c6114a | |
|
|
8cb0c760b6 | |
|
|
adb68be243 | |
|
|
c56cab55c4 | |
|
|
4d836a4b80 | |
|
|
c490967a23 | |
|
|
475c7de10f | |
|
|
6f5e1d2277 | |
|
|
503645f3cf | |
|
|
869f9d25f5 | |
|
|
38ff63772b | |
|
|
f464b5923d | |
|
|
8c35fb532c | |
|
|
775ef67955 | |
|
|
0e7fd175f0 | |
|
|
238f24a5f6 | |
|
|
2e66c1cce7 | |
|
|
5412374e02 | |
|
|
82de887596 | |
|
|
24498b54d7 | |
|
|
41e2c0dc06 | |
|
|
119db20187 | |
|
|
aee638f5b2 | |
|
|
6bd3079f62 | |
|
|
02c6ea6191 | |
|
|
680af40054 | |
|
|
813068a21f | |
|
|
3d774d57d0 | |
|
|
361905127b | |
|
|
59b266ca24 | |
|
|
900afa16e6 | |
|
|
d15057b9fc | |
|
|
43cd092fcc | |
|
|
894e72016a | |
|
|
1aadb6e1fe | |
|
|
f363b49bbd | |
|
|
0c123a676b | |
|
|
fced2226b3 | |
|
|
d056663d40 | |
|
|
46c57f2faf | |
|
|
283ede377d | |
|
|
fd3e37be3b | |
|
|
3bc422cd8a | |
|
|
3908749a84 | |
|
|
a500cbd327 | |
|
|
1578849631 | |
|
|
c7a88f838a | |
|
|
be3e546127 | |
|
|
eec8fa1129 |
33
.cirrus.yml
33
.cirrus.yml
|
|
@ -5,11 +5,10 @@ env:
|
|||
####
|
||||
#### Global variables used for all tasks
|
||||
####
|
||||
GOPATH: "/var/tmp/go"
|
||||
CONMON_SLUG: "github.com/containers/conmon"
|
||||
|
||||
# Overrides default location (/tmp/cirrus) for repo clone (will become $SRC)
|
||||
CIRRUS_WORKING_DIR: "${GOPATH}/src/${CONMON_SLUG}"
|
||||
CIRRUS_WORKING_DIR: "/var/tmp/src/${CONMON_SLUG}"
|
||||
# Required so $ENVLIB gets loaded and /bin/sh is not used
|
||||
CIRRUS_SHELL: "/bin/bash"
|
||||
# Save a little typing (path relative to $CIRRUS_WORKING_DIR)
|
||||
|
|
@ -17,14 +16,8 @@ env:
|
|||
# Spoof self as travis, as cirrus has the same test issues as travis does
|
||||
TRAVIS: "true"
|
||||
|
||||
####
|
||||
#### Cache-image names to test with (double-quotes around names are critical)
|
||||
####
|
||||
FEDORA_NAME: "fedora-39ß"
|
||||
PRIOR_FEDORA_NAME: "fedora-38"
|
||||
|
||||
# VM Image built in containers/automation_images
|
||||
IMAGE_SUFFIX: "c20240102t155643z-f39f38d13"
|
||||
IMAGE_SUFFIX: "c20250721t181111z-f42f41d13"
|
||||
FEDORA_CACHE_IMAGE_NAME: "fedora-${IMAGE_SUFFIX}"
|
||||
|
||||
# Container FQIN's
|
||||
|
|
@ -61,28 +54,28 @@ fedora_packaging_task:
|
|||
memory: 4
|
||||
|
||||
matrix:
|
||||
- name: "Packaging for ${FEDORA_NAME}"
|
||||
- name: "Packaging for Fedora"
|
||||
container:
|
||||
image: "${FEDORA_CONTAINER_FQIN}"
|
||||
- name: "Packaging for ${PRIOR_FEDORA_NAME}"
|
||||
- name: "Packaging for Fedora N-1"
|
||||
container:
|
||||
image: "${PRIOR_FEDORA_CONTAINER_FQIN}"
|
||||
|
||||
script:
|
||||
- dnf install -y rpm-build golang libseccomp-devel
|
||||
- dnf install -y rpm-build libseccomp-devel
|
||||
- cd $CIRRUS_WORKING_DIR
|
||||
- make
|
||||
- make -f .rpmbuild/Makefile
|
||||
- rpmbuild --rebuild conmon-*.src.rpm
|
||||
- dnf erase -y conmon
|
||||
- dnf remove -y conmon
|
||||
- dnf -y install ~/rpmbuild/RPMS/x86_64/conmon*.x86_64.rpm
|
||||
- ls -l /usr/bin/conmon
|
||||
|
||||
timeout_in: '20m'
|
||||
|
||||
|
||||
# Verify calls to bin/config were saved
|
||||
config_task:
|
||||
# Verify build completes successfully
|
||||
build_task:
|
||||
# Runs within Cirrus's "community cluster"
|
||||
container:
|
||||
image: "${FEDORA_CONTAINER_FQIN}"
|
||||
|
|
@ -90,10 +83,10 @@ config_task:
|
|||
memory: 4
|
||||
|
||||
script:
|
||||
- dnf install -y make glib2-devel git gcc golang
|
||||
- dnf install -y make glib2-devel git gcc pkg-config systemd-devel libseccomp-devel
|
||||
- cd $CIRRUS_WORKING_DIR
|
||||
- make config
|
||||
- ./hack/tree_status.sh
|
||||
- make
|
||||
- make test
|
||||
|
||||
|
||||
# Verify code was fmt'ed properly
|
||||
|
|
@ -105,10 +98,10 @@ fmt_task:
|
|||
memory: 4
|
||||
|
||||
script:
|
||||
- dnf install -y clang clang-tools-extra golang
|
||||
- dnf install -y clang clang-tools-extra
|
||||
- cd $CIRRUS_WORKING_DIR
|
||||
- make fmt
|
||||
- ./hack/tree_status.sh
|
||||
- git diff --exit-code
|
||||
|
||||
|
||||
# Build the static binary
|
||||
|
|
|
|||
|
|
@ -4,50 +4,55 @@ on:
|
|||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
||||
conmon:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/.cache/go-build
|
||||
key: go-integration-conmon-${{ hashFiles('**/go.mod') }}
|
||||
restore-keys: go-integration-conmon-
|
||||
- run: hack/github-actions-setup
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install BATS
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y bats
|
||||
- run: sudo hack/github-actions-setup
|
||||
- name: Run conmon integration tests
|
||||
run: |
|
||||
sudo make vendor
|
||||
sudo mkdir -p /var/run/crio
|
||||
sudo make test-binary
|
||||
|
||||
cri-o:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [stable, oldstable]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/.cache/go-build
|
||||
key: go-integration-cri-o-${{ hashFiles('**/go.mod') }}
|
||||
restore-keys: go-integration-cri-o-
|
||||
- run: hack/github-actions-setup
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: false
|
||||
- name: Install BATS
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y bats
|
||||
- run: sudo hack/github-actions-setup
|
||||
- name: Run CRI-O integration tests
|
||||
run: |
|
||||
cd $(go env GOPATH)/src/github.com/cri-o/cri-o
|
||||
make all test-binaries
|
||||
CRIO_DIR=$(sudo go env GOPATH)/src/github.com/cri-o/cri-o
|
||||
sudo make -C "$CRIO_DIR" all test-binaries
|
||||
# skip seccomp tests because they have permission denied issues in a container and accept signed image as they don't use conmon
|
||||
sudo -E test/test_runner.sh $(ls test/ | grep bats | grep -E -v seccomp\|image\|policy)
|
||||
sudo rm -f "$CRIO_DIR"/test/seccomp*.bats "$CRIO_DIR"/test/image.bats "$CRIO_DIR"/test/policy.bats
|
||||
sudo sh -c "cd $CRIO_DIR; ./test/test_runner.sh"
|
||||
env:
|
||||
JOBS: '2'
|
||||
|
||||
all-done:
|
||||
needs:
|
||||
- conmon
|
||||
- cri-o
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- run: echo "All jobs completed"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
name: validate
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
pull_request:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check C code formatting
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang-format
|
||||
make fmt
|
||||
git diff --exit-code
|
||||
|
||||
all-done:
|
||||
needs:
|
||||
- lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "All jobs completed"
|
||||
|
|
@ -78,10 +78,12 @@ jobs:
|
|||
|
||||
- job: koji_build
|
||||
trigger: commit
|
||||
packages: [conmon-fedora]
|
||||
dist_git_branches:
|
||||
- fedora-all
|
||||
|
||||
- job: bodhi_update
|
||||
trigger: commit
|
||||
packages: [conmon-fedora]
|
||||
dist_git_branches:
|
||||
- fedora-branched # rawhide updates are created automatically
|
||||
|
|
|
|||
37
Makefile
37
Makefile
|
|
@ -2,8 +2,6 @@ VERSION := $(shell cat VERSION)
|
|||
PREFIX ?= /usr/local
|
||||
BINDIR ?= ${PREFIX}/bin
|
||||
LIBEXECDIR ?= ${PREFIX}/libexec
|
||||
GO ?= go
|
||||
PROJECT := github.com/containers/conmon
|
||||
PKG_CONFIG ?= pkg-config
|
||||
HEADERS := $(wildcard src/*.h)
|
||||
|
||||
|
|
@ -38,10 +36,14 @@ override CFLAGS += $(shell $(PKG_CONFIG) --cflags glib-2.0) -DVERSION=\"$(VERSIO
|
|||
# "pkg-config --exists" will error if the package doesn't exist. Make can only compare
|
||||
# output of commands, so the echo commands are to allow pkg-config to error out, make to catch it,
|
||||
# and allow the compilation to complete.
|
||||
#
|
||||
# For static builds, systemd can be disabled with DISABLE_SYSTEMD=1
|
||||
ifneq ($(DISABLE_SYSTEMD), 1)
|
||||
ifeq ($(shell $(PKG_CONFIG) --exists libsystemd && echo "0"), 0)
|
||||
override LIBS += $(shell $(PKG_CONFIG) --libs libsystemd)
|
||||
override CFLAGS += $(shell $(PKG_CONFIG) --cflags libsystemd) -D USE_JOURNALD=1
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(shell hack/seccomp-notify.sh), 0)
|
||||
override LIBS += $(shell $(PKG_CONFIG) --libs libseccomp) -ldl
|
||||
|
|
@ -67,34 +69,22 @@ bin/conmon: $(OBJS) | bin
|
|||
%.o: %.c $(HEADERS)
|
||||
$(CC) $(CFLAGS) $(DEBUGFLAG) -o $@ -c $<
|
||||
|
||||
config: git-vars cmd/conmon-config/conmon-config.go runner/config/config.go runner/config/config_unix.go runner/config/config_windows.go
|
||||
$(GO) build $(LDFLAGS) -tags "$(BUILDTAGS)" -o bin/config $(PROJECT)/cmd/conmon-config
|
||||
( cd src && $(CURDIR)/bin/config )
|
||||
# config target removed - no longer using Go build system
|
||||
|
||||
.PHONY: test-binary
|
||||
test-binary: bin/conmon _test-files
|
||||
CONMON_BINARY="$(MAKEFILE_PATH)bin/conmon" $(GO) test $(LDFLAGS) -tags "$(BUILDTAGS)" $(PROJECT)/runner/conmon_test/ -count=1 -v
|
||||
test-binary: bin/conmon
|
||||
CONMON_BINARY="$(MAKEFILE_PATH)bin/conmon" test/run-tests.sh
|
||||
|
||||
.PHONY: test
|
||||
test:_test-files
|
||||
$(GO) test $(LDFLAGS) -tags "$(BUILDTAGS)" $(PROJECT)/runner/conmon_test/
|
||||
|
||||
.PHONY: test-files
|
||||
_test-files: git-vars runner/conmon_test/*.go runner/conmon/*.go
|
||||
test: bin/conmon
|
||||
CONMON_BINARY="$(MAKEFILE_PATH)bin/conmon" test/run-tests.sh
|
||||
|
||||
bin:
|
||||
mkdir -p bin
|
||||
|
||||
.PHONY: vendor
|
||||
vendor:
|
||||
GO111MODULE=on $(GO) mod tidy
|
||||
GO111MODULE=on $(GO) mod vendor
|
||||
GO111MODULE=on $(GO) mod verify
|
||||
# vendor target removed - no longer using Go modules
|
||||
|
||||
.PHONY: docs
|
||||
ifeq ($(GOMD2MAN),)
|
||||
docs: install.tools
|
||||
endif
|
||||
docs:
|
||||
$(MAKE) -C docs
|
||||
|
||||
|
|
@ -123,14 +113,9 @@ install.podman: bin/conmon
|
|||
install ${SELINUXOPT} -d -m 755 $(DESTDIR)$(LIBEXECDIR)/podman
|
||||
install ${SELINUXOPT} -m 755 bin/conmon $(DESTDIR)$(LIBEXECDIR)/podman/conmon
|
||||
|
||||
install.tools:
|
||||
$(MAKE) -C tools
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
find . '(' -name '*.h' -o -name '*.c' ! -path './vendor/*' ! -path './tools/vendor/*' ')' -exec clang-format -i {} \+
|
||||
find . -name '*.go' ! -path './vendor/*' ! -path './tools/vendor/*' -exec gofmt -s -w {} \+
|
||||
git diff --exit-code
|
||||
git ls-files -z \*.c \*.h | xargs -0 clang-format -i
|
||||
|
||||
|
||||
.PHONY: dbuild
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
|
||||
"github.com/containers/conmon/runner/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
output := `
|
||||
#if !defined(CONFIG_H)
|
||||
#define CONFIG_H
|
||||
|
||||
#define BUF_SIZE %d
|
||||
#define STDIO_BUF_SIZE %d
|
||||
#define CONN_SOCK_BUF_SIZE %d
|
||||
#define DEFAULT_SOCKET_PATH "%s"
|
||||
#define WIN_RESIZE_EVENT %d
|
||||
#define REOPEN_LOGS_EVENT %d
|
||||
#define TIMED_OUT_MESSAGE "%s"
|
||||
|
||||
#endif // CONFIG_H
|
||||
`
|
||||
if err := ioutil.WriteFile("config.h", []byte(fmt.Sprintf(
|
||||
output,
|
||||
config.BufSize,
|
||||
config.BufSize,
|
||||
config.ConnSockBufSize,
|
||||
config.ContainerAttachSocketDir,
|
||||
config.WinResizeEvent,
|
||||
config.ReopenLogsEvent,
|
||||
config.TimedOutMessage)),
|
||||
0644); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
PREFIX ?= /usr/local
|
||||
DATADIR := ${PREFIX}/share
|
||||
MANDIR := $(DATADIR)/man
|
||||
GOMD2MAN ?= ../tools/build/go-md2man
|
||||
GOMD2MAN ?= go-md2man
|
||||
|
||||
docs: $(patsubst %.md,%,$(wildcard *.8.md))
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ Path to the process spec for execution.
|
|||
Path to the program to execute when the container terminates its execution.
|
||||
|
||||
**--exit-command-arg**
|
||||
Additional arguments to pass to the exit command. Can be specified multiple time.
|
||||
Additional arguments to pass to the exit command. Can be specified multiple times.
|
||||
|
||||
**--exit-delay**
|
||||
Delay before invoking the exit command (in seconds).
|
||||
|
|
@ -64,11 +64,15 @@ Leave stdin open when the attached client disconnects.
|
|||
Print debug logs based on the log level.
|
||||
|
||||
**--log-size-max**
|
||||
Maximum size of the log file.
|
||||
Maximum size of the log file (in bytes).
|
||||
|
||||
**--log-tag**
|
||||
Additional tag to use for logging.
|
||||
|
||||
**--log-label**
|
||||
Additional label to use for logging. The accepted format is LABEL=VALUE. Can be specified multiple times.
|
||||
Note that LABEL must contain only uppercase letters, numbers and underscore character.
|
||||
|
||||
**-n**, **--name**
|
||||
Container name.
|
||||
|
||||
|
|
@ -94,7 +98,7 @@ PID file for the conmon process.
|
|||
Path to store runtime data for the container.
|
||||
|
||||
**--replace-listen-pid**
|
||||
Replace listen pid if set for oci-runtime pid.
|
||||
Replace listen PID if set for oci-runtime PID.
|
||||
|
||||
**--restore**
|
||||
Restore a container from a checkpoint.
|
||||
|
|
@ -106,7 +110,7 @@ Additional arguments to pass to the runtime. Can be specified multiple times.
|
|||
Additional options to pass to the restore or exec command. Can be specified multiple times.
|
||||
|
||||
**-s**, **--systemd-cgroup**
|
||||
Enable systemd cgroup manager, rather then use the cgroupfs directly.
|
||||
Enable systemd cgroup manager, rather than use the cgroupfs directly.
|
||||
|
||||
**--socket-dir-path**
|
||||
Location of container attach sockets.
|
||||
|
|
|
|||
43
go.mod
43
go.mod
|
|
@ -1,43 +0,0 @@
|
|||
module github.com/containers/conmon
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/containers/podman/v4 v4.5.0
|
||||
github.com/containers/storage v1.48.0
|
||||
github.com/coreos/go-systemd/v22 v22.5.0
|
||||
github.com/onsi/ginkgo/v2 v2.15.0
|
||||
github.com/onsi/gomega v1.31.1
|
||||
github.com/opencontainers/runtime-tools v0.9.1-0.20230914150019-408c51e934dc
|
||||
github.com/pkg/errors v0.9.1
|
||||
golang.org/x/sys v0.15.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/containers/common v0.52.0 // indirect
|
||||
github.com/containers/image/v5 v5.25.0 // indirect
|
||||
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
|
||||
github.com/containers/ocicrypt v1.1.7 // indirect
|
||||
github.com/docker/docker v24.0.7+incompatible // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/go-logr/logr v1.3.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/moby/sys/mountinfo v0.6.2 // indirect
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect
|
||||
github.com/opencontainers/runc v1.1.7 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.1.0-rc.3 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.16.1 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
191
go.sum
191
go.sum
|
|
@ -1,191 +0,0 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/containers/common v0.52.0 h1:S5GApgpNEGBuPhDHTFgMc55y5gsuxHcQeElvUpO5kp4=
|
||||
github.com/containers/common v0.52.0/go.mod h1:dNJJVNBu1wJtAH+vFIMXV+fQHBdEVNmNP3ImjbKper4=
|
||||
github.com/containers/image/v5 v5.25.0 h1:TJ0unmalbU+scd0i3Txap2wjGsAnv06MSCwgn6bsizk=
|
||||
github.com/containers/image/v5 v5.25.0/go.mod h1:EKvys0WVlRFkDw26R8y52TuhV9Tfn0yq2luLX6W52Ls=
|
||||
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA=
|
||||
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY=
|
||||
github.com/containers/ocicrypt v1.1.7 h1:thhNr4fu2ltyGz8aMx8u48Ae0Pnbip3ePP9/mzkZ/3U=
|
||||
github.com/containers/ocicrypt v1.1.7/go.mod h1:7CAhjcj2H8AYp5YvEie7oVSK2AhBY8NscCYRawuDNtw=
|
||||
github.com/containers/podman/v4 v4.5.0 h1:fgZmKgyscYXSx+ddflv0PI45Co+QZfpYcm47I6M06w4=
|
||||
github.com/containers/podman/v4 v4.5.0/go.mod h1:BoNmT1QNzMtDMUCiJ1j1ZoDx6OOn5BATBih6sfg7pJs=
|
||||
github.com/containers/storage v1.48.0 h1:wiPs8J2xiFoOEAhxHDRtP6A90Jzj57VqzLRXOqeizns=
|
||||
github.com/containers/storage v1.48.0/go.mod h1:pRp3lkRo2qodb/ltpnudoXggrviRmaCmU5a5GhTBae0=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
|
||||
github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
||||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
|
||||
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY=
|
||||
github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM=
|
||||
github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo=
|
||||
github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b h1:YWuSjZCQAPM8UUBLkYUk1e+rZcvWHJmFb6i6rM44Xs8=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
|
||||
github.com/opencontainers/runc v1.1.7 h1:y2EZDS8sNng4Ksf0GUYNhKbTShZJPJg1FiXJNH/uoCk=
|
||||
github.com/opencontainers/runc v1.1.7/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50=
|
||||
github.com/opencontainers/runtime-spec v1.1.0-rc.3 h1:l04uafi6kxByhbxev7OWiuUv0LZxEsYUfDWZ6bztAuU=
|
||||
github.com/opencontainers/runtime-spec v1.1.0-rc.3/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/runtime-tools v0.9.1-0.20230914150019-408c51e934dc h1:d2hUh5O6MRBvStV55MQ8we08t42zSTqBbscoQccWmMc=
|
||||
github.com/opencontainers/runtime-tools v0.9.1-0.20230914150019-408c51e934dc/go.mod h1:8tx1helyqhUC65McMm3x7HmOex8lO2/v9zPuxmKHurs=
|
||||
github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
|
||||
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||
github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
|
||||
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
|
@ -3,7 +3,8 @@ set -euo pipefail
|
|||
|
||||
declare -A VERSIONS=(
|
||||
["cni-plugins"]=v1.3.0
|
||||
["runc"]=v1.1.7
|
||||
["runc"]=v1.1.14
|
||||
["crun"]=1.17
|
||||
["bats"]=v1.9.0
|
||||
)
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ main() {
|
|||
install_bats
|
||||
install_critools
|
||||
install_runc
|
||||
install_crun
|
||||
install_cni_plugins
|
||||
install_testdeps
|
||||
setup_etc_subid
|
||||
|
|
@ -32,6 +34,7 @@ prepare_system() {
|
|||
sudo sysctl -w net.ipv4.conf.all.route_localnet=1
|
||||
sudo sysctl -w net.ipv4.ip_forward=1
|
||||
# needed for crictl test
|
||||
sudo modprobe br_netfilter
|
||||
sudo sysctl -w net.bridge.bridge-nf-call-iptables=1
|
||||
sudo iptables -t nat -I POSTROUTING -s 127.0.0.0/8 ! -d 127.0.0.0/8 -j MASQUERADE
|
||||
}
|
||||
|
|
@ -43,9 +46,18 @@ remove_packages() {
|
|||
}
|
||||
|
||||
install_packages() {
|
||||
. /etc/os-release
|
||||
CRIU_REPO="https://download.opensuse.org/repositories/devel:/tools:/criu/xUbuntu_$VERSION_ID"
|
||||
|
||||
curl -fSsL $CRIU_REPO/Release.key | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/criu.gpg
|
||||
echo "deb $CRIU_REPO/ /" | sudo tee /etc/apt/sources.list.d/criu.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install -y \
|
||||
autoconf \
|
||||
automake \
|
||||
conntrack \
|
||||
criu \
|
||||
libaio-dev \
|
||||
libapparmor-dev \
|
||||
libbtrfs-dev \
|
||||
|
|
@ -53,19 +65,23 @@ install_packages() {
|
|||
libdevmapper-dev \
|
||||
libfuse-dev \
|
||||
libgpgme11-dev \
|
||||
libglib2.0-dev \
|
||||
libnet1-dev \
|
||||
libnl-3-dev \
|
||||
libprotobuf-c-dev \
|
||||
libprotobuf-dev \
|
||||
libseccomp-dev \
|
||||
libsystemd-dev \
|
||||
libtool \
|
||||
libudev-dev \
|
||||
libyajl-dev \
|
||||
sed \
|
||||
socat \
|
||||
uuid-dev
|
||||
}
|
||||
|
||||
install_conmon() {
|
||||
sudo make install
|
||||
sudo make install.bin
|
||||
conmon --version
|
||||
}
|
||||
|
||||
|
|
@ -118,6 +134,16 @@ install_runc() {
|
|||
runc --version
|
||||
}
|
||||
|
||||
install_crun() {
|
||||
URL=https://github.com/containers/crun/releases/download/"${VERSIONS["crun"]}"/crun-"${VERSIONS["crun"]}"-linux-amd64
|
||||
|
||||
BINARY=/usr/bin/crun
|
||||
sudo wget -O "$BINARY" "$URL"
|
||||
sudo chmod +x "$BINARY"
|
||||
|
||||
crun --version
|
||||
}
|
||||
|
||||
install_testdeps() {
|
||||
CLONE_PATH=$(go env GOPATH)/src/github.com/cri-o
|
||||
mkdir -p "$CLONE_PATH"
|
||||
|
|
|
|||
|
|
@ -1,99 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Check for root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "Please run this script as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install latest CRI-O
|
||||
curl https://raw.githubusercontent.com/cri-o/cri-o/master/scripts/get | bash
|
||||
crio --version
|
||||
|
||||
# Use runc as default runtime
|
||||
rm /etc/crio/crio.conf.d/10-crun.conf
|
||||
cat <<EOT >>/etc/crio/crio.conf.d/10-config.conf
|
||||
[crio.runtime]
|
||||
log_level = "debug"
|
||||
[crio.runtime.runtimes.runc]
|
||||
runtime_path = "/usr/local/bin/runc"
|
||||
EOT
|
||||
runc --version
|
||||
|
||||
# Start CRI-O
|
||||
systemctl daemon-reload
|
||||
systemctl start crio
|
||||
systemctl is-active --quiet crio && echo CRI-O is running
|
||||
journalctl --no-pager -u crio
|
||||
|
||||
# Start Kubernetes
|
||||
GOROOT="$GOROOT_1_17_X64"
|
||||
GOPATH="$HOME/go"
|
||||
PATH="$GOROOT/bin:$PATH"
|
||||
K8SPATH="$GOPATH/src/k8s.io"
|
||||
mkdir -p "$K8SPATH"
|
||||
pushd "$K8SPATH"
|
||||
git clone --depth=1 https://github.com/kubernetes/kubernetes
|
||||
pushd kubernetes
|
||||
|
||||
LATEST=$(curl -sSfL https://dl.k8s.io/ci/latest.txt)
|
||||
echo "Using Kubernetes build $LATEST"
|
||||
|
||||
URL="https://dl.k8s.io/ci/$LATEST"
|
||||
mkdir -p _output/bin
|
||||
|
||||
echo Downloading server binaries
|
||||
curl -sSfL "$URL/kubernetes-server-linux-amd64.tar.gz" -o- | tar xfz -
|
||||
cp kubernetes/server/bin/{kubectl,kube-apiserver,kube-controller-manager,kube-proxy,kube-scheduler,kubelet} \
|
||||
_output/bin
|
||||
|
||||
echo Downloading test binaries
|
||||
curl -sSfL "$URL/kubernetes-test-linux-amd64.tar.gz" -o- | tar xfz -
|
||||
cp kubernetes/test/bin/{ginkgo,e2e.test} _output/bin
|
||||
|
||||
export CONTAINER_RUNTIME=remote
|
||||
export CGROUP_DRIVER=systemd
|
||||
export CGROUP_ROOT=/
|
||||
export KUBELET_FLAGS='--runtime-cgroups=/system.slice/crio.service --non-masquerade-cidr=0.0.0.0/0'
|
||||
export CONTAINER_RUNTIME_ENDPOINT=/var/run/crio/crio.sock
|
||||
export ALLOW_PRIVILEGED=1
|
||||
|
||||
IP=$(ip route get 1.2.3.4 | cut -d ' ' -f7 | tr -d '[:space:]')
|
||||
echo "Using IP: $IP"
|
||||
export DNS_SERVER_IP=$IP
|
||||
export API_HOST_IP=$IP
|
||||
|
||||
iptables -F
|
||||
hack/install-etcd.sh
|
||||
export PATH="$GOPATH/src/k8s.io/kubernetes/third_party/etcd:$PATH"
|
||||
|
||||
OUTPUT=$(mktemp)
|
||||
hack/local-up-cluster.sh -O 2>&1 | tee "$OUTPUT" &
|
||||
PID=$!
|
||||
|
||||
until grep -q "Local Kubernetes cluster is running" "$OUTPUT"; do
|
||||
echo Waiting for hack/local-up-cluster.sh
|
||||
if ! ps $PID >/dev/null; then
|
||||
exit 1
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
echo Cluster is up and running
|
||||
|
||||
# Run the tests
|
||||
export KUBERUN=/var/run/kubernetes
|
||||
export KUBECONFIG=$KUBERUN/admin.kubeconfig
|
||||
export PATH="$GOPATH/src/k8s.io/kubernetes/_output/local/bin/linux/amd64:$PATH"
|
||||
export KUBE_MASTER_URL=$IP
|
||||
export KUBE_MASTER_IP=$IP
|
||||
export KUBE_MASTER=$IP
|
||||
export HOSTNAME_OVERRIDE=$IP
|
||||
|
||||
export GINKGO_PARALLEL_NODES=4
|
||||
export GINKGO_PARALLEL=y
|
||||
|
||||
_output/bin/e2e.test --provider=local --host="https://$IP:6443" \
|
||||
--ginkgo.focus='\[NodeConformance|NodeFeature:.*\]' \
|
||||
--ginkgo.skip='\[Flaky|Slow|Serial|sig-network|NodeFeature:RuntimeHandler\]|exec hook properly'
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
#!/bin/bash
|
||||
# this script is based off of the similarly named in github.com/containers/libpod/hack/tree_status.sh
|
||||
|
||||
set -e
|
||||
|
||||
SUGGESTION="${SUGGESTION:-call 'make config' and commit all changes.}"
|
||||
|
||||
STATUS=$(git status --porcelain)
|
||||
if [[ -z $STATUS ]]
|
||||
then
|
||||
echo "tree is clean"
|
||||
else
|
||||
echo "tree is dirty, please $SUGGESTION"
|
||||
echo ""
|
||||
echo "$STATUS"
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
{ stdenv
|
||||
, pkgs
|
||||
, enableSystemd ? false
|
||||
}:
|
||||
with pkgs; stdenv.mkDerivation rec {
|
||||
name = "conmon";
|
||||
src = ./..;
|
||||
# Use Pure to avoid exuding the .git directory
|
||||
src = nix-gitignore.gitignoreSourcePure [ ../.gitignore ] ./..;
|
||||
vendorHash = null;
|
||||
doCheck = false;
|
||||
enableParallelBuilding = true;
|
||||
|
|
@ -17,11 +19,15 @@ with pkgs; stdenv.mkDerivation rec {
|
|||
] ++ [
|
||||
pkgsStatic.glib
|
||||
libseccomp
|
||||
] ++ lib.optionals enableSystemd [
|
||||
# Only include systemd for dynamic builds, not static builds
|
||||
# Static builds will use PKG_CONFIG_PATH approach instead
|
||||
];
|
||||
prePatch = ''
|
||||
export CFLAGS='-static -pthread'
|
||||
export LDFLAGS='-s -w -static-libgcc -static'
|
||||
export EXTRA_LDFLAGS='-s -w -linkmode external -extldflags "-static -lm"'
|
||||
${lib.optionalString (!enableSystemd) "export DISABLE_SYSTEMD=1"}
|
||||
'';
|
||||
buildPhase = ''
|
||||
patchShebangs .
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ Epoch: 3
|
|||
%else
|
||||
Epoch: 2
|
||||
%endif
|
||||
Version: 2.1.12
|
||||
Version: 2.1.13
|
||||
License: Apache-2.0
|
||||
Release: %autorelease
|
||||
Summary: OCI container runtime monitor
|
||||
|
|
@ -27,13 +27,13 @@ URL: https://github.com/containers/%{name}
|
|||
# Tarball fetched from upstream
|
||||
Source0: %{url}/archive/v%{version}.tar.gz
|
||||
%if %{with docs}
|
||||
ExclusiveArch: %{golang_arches_future}
|
||||
BuildRequires: go-md2man
|
||||
%endif
|
||||
BuildRequires: gcc
|
||||
BuildRequires: git-core
|
||||
BuildRequires: glib2-devel
|
||||
BuildRequires: libseccomp-devel
|
||||
BuildRequires: pkgconfig
|
||||
BuildRequires: systemd-devel
|
||||
BuildRequires: systemd-libs
|
||||
BuildRequires: make
|
||||
|
|
@ -47,7 +47,6 @@ Requires: libseccomp
|
|||
%prep
|
||||
%autosetup -Sgit %{name}-%{version}
|
||||
sed -i 's/install.bin: bin\/conmon/install.bin:/' Makefile
|
||||
sed -i 's/install.crio: bin\/conmon/install.crio:/' Makefile
|
||||
|
||||
%build
|
||||
%{__make} DEBUGFLAG="-g" bin/conmon
|
||||
|
|
@ -57,7 +56,7 @@ sed -i 's/install.crio: bin\/conmon/install.crio:/' Makefile
|
|||
%endif
|
||||
|
||||
%install
|
||||
%{__make} PREFIX=%{buildroot}%{_prefix} install.bin install.crio
|
||||
%{__make} PREFIX=%{buildroot}%{_prefix} install.bin
|
||||
|
||||
%if %{with docs}
|
||||
%{__make} PREFIX=%{buildroot}%{_prefix} -C docs install
|
||||
|
|
@ -70,17 +69,10 @@ sed -i 's/install.crio: bin\/conmon/install.crio:/' Makefile
|
|||
%license LICENSE
|
||||
%doc README.md
|
||||
%{_bindir}/%{name}
|
||||
%{_libexecdir}/crio/%{name}
|
||||
%dir %{_libexecdir}/crio
|
||||
|
||||
%if %{with docs}
|
||||
%{_mandir}/man8/%{name}.8.gz
|
||||
%endif
|
||||
|
||||
%changelog
|
||||
%if %{defined autochangelog}
|
||||
%autochangelog
|
||||
%else
|
||||
* Fri Jan 26 2024 RH Container Bot <rhcontainerbot@fedoraproject.org>
|
||||
- Placeholder changelog for envs that are not autochangelog-ready
|
||||
%endif
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
package config
|
||||
|
||||
const (
|
||||
// BufSize is the size of buffers passed in to sockets
|
||||
BufSize = 8192
|
||||
// ConnSockBufSize is the size of the socket used for
|
||||
// to attach to the container
|
||||
ConnSockBufSize = 32768
|
||||
// WinResizeEvent is the event code the caller program will
|
||||
// send along the ctrl fd to signal conmon to resize
|
||||
// the pty window
|
||||
WinResizeEvent = 1
|
||||
// ReopenLogsEvent is the event code the caller program will
|
||||
// send along the ctrl fd to signal conmon to reopen the log files
|
||||
ReopenLogsEvent = 2
|
||||
// TimedOutMessage is the message sent back to the caller by conmon
|
||||
// when a container times out
|
||||
TimedOutMessage = "command timed out"
|
||||
)
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
//go:build !windows
|
||||
|
||||
package config
|
||||
|
||||
const (
|
||||
ContainerAttachSocketDir = "/var/run/crio"
|
||||
)
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
//go:build windows
|
||||
|
||||
package config
|
||||
|
||||
const (
|
||||
ContainerAttachSocketDir = "C:\\crio\\run\\"
|
||||
)
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
package conmon
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrConmonNotStarted = errors.New("conmon instance is not started")
|
||||
)
|
||||
|
||||
type ConmonInstance struct {
|
||||
args []string
|
||||
cmd *exec.Cmd
|
||||
started bool
|
||||
path string
|
||||
pidFile string
|
||||
stdout io.Writer
|
||||
stderr io.Writer
|
||||
stdin io.Reader
|
||||
|
||||
parentStartPipe *os.File
|
||||
parentAttachPipe *os.File
|
||||
parentSyncPipe *os.File
|
||||
childSyncPipe *os.File
|
||||
childStartPipe *os.File
|
||||
childAttachPipe *os.File
|
||||
}
|
||||
|
||||
func CreateAndExecConmon(options ...ConmonOption) (*ConmonInstance, error) {
|
||||
ci, err := NewConmonInstance(options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ci.Start()
|
||||
return ci, nil
|
||||
}
|
||||
|
||||
func NewConmonInstance(options ...ConmonOption) (*ConmonInstance, error) {
|
||||
ci := &ConmonInstance{
|
||||
args: make([]string, 0),
|
||||
}
|
||||
for _, option := range options {
|
||||
if err := option(ci); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO verify path more
|
||||
if ci.path == "" {
|
||||
return nil, errors.New("conmon path not specified")
|
||||
}
|
||||
|
||||
ci.cmd = exec.Command(ci.path, ci.args...)
|
||||
ci.configurePipeEnv()
|
||||
|
||||
ci.cmd.Stdout = ci.stdout
|
||||
ci.cmd.Stderr = ci.stderr
|
||||
ci.cmd.Stdin = ci.stdin
|
||||
return ci, nil
|
||||
}
|
||||
|
||||
func (ci *ConmonInstance) Start() error {
|
||||
ci.started = true
|
||||
return ci.cmd.Start()
|
||||
}
|
||||
|
||||
func (ci *ConmonInstance) Wait() error {
|
||||
if !ci.started {
|
||||
return ErrConmonNotStarted
|
||||
}
|
||||
defer func() {
|
||||
ci.childSyncPipe.Close()
|
||||
ci.childStartPipe.Close()
|
||||
ci.childAttachPipe.Close()
|
||||
}()
|
||||
return ci.cmd.Wait()
|
||||
}
|
||||
|
||||
func (ci *ConmonInstance) Stdout() (io.Writer, error) {
|
||||
if !ci.started {
|
||||
return nil, ErrConmonNotStarted
|
||||
}
|
||||
return ci.cmd.Stdout, nil
|
||||
}
|
||||
|
||||
func (ci *ConmonInstance) Stderr() (io.Writer, error) {
|
||||
if !ci.started {
|
||||
return nil, ErrConmonNotStarted
|
||||
}
|
||||
return ci.cmd.Stderr, nil
|
||||
}
|
||||
|
||||
func (ci *ConmonInstance) Pid() (int, error) {
|
||||
if ci.pidFile == "" {
|
||||
return -1, errors.Errorf("conmon pid file not specified")
|
||||
}
|
||||
if !ci.started {
|
||||
return -1, ErrConmonNotStarted
|
||||
}
|
||||
|
||||
pid, err := readConmonPidFile(ci.pidFile)
|
||||
if err != nil {
|
||||
return -1, errors.Wrapf(err, "failed to find conmon pid file")
|
||||
}
|
||||
return pid, nil
|
||||
}
|
||||
|
||||
// readConmonPidFile attempts to read conmon's pid from its pid file
|
||||
func readConmonPidFile(pidFile string) (int, error) {
|
||||
// Let's try reading the Conmon pid at the same time.
|
||||
if pidFile != "" {
|
||||
contents, err := ioutil.ReadFile(pidFile)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
// Convert it to an int
|
||||
conmonPID, err := strconv.Atoi(string(contents))
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return conmonPID, nil
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (ci *ConmonInstance) Cleanup() {
|
||||
ci.closePipesOnCleanup()
|
||||
}
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
package conmon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type ConmonOption func(*ConmonInstance) error
|
||||
|
||||
func WithVersion() ConmonOption {
|
||||
return func(ci *ConmonInstance) error {
|
||||
return ci.addArgs("--version")
|
||||
}
|
||||
}
|
||||
|
||||
func WithStdout(stdout io.Writer) ConmonOption {
|
||||
return func(ci *ConmonInstance) error {
|
||||
ci.stdout = stdout
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithStderr(stderr io.Writer) ConmonOption {
|
||||
return func(ci *ConmonInstance) error {
|
||||
ci.stderr = stderr
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithStdin(stdin io.Reader) ConmonOption {
|
||||
return func(ci *ConmonInstance) error {
|
||||
ci.stdin = stdin
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithPath(path string) ConmonOption {
|
||||
return func(ci *ConmonInstance) error {
|
||||
ci.path = path
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithContainerID(ctrID string) ConmonOption {
|
||||
return func(ci *ConmonInstance) error {
|
||||
return ci.addArgs("--cid", ctrID)
|
||||
}
|
||||
}
|
||||
|
||||
func WithContainerUUID(ctrUUID string) ConmonOption {
|
||||
return func(ci *ConmonInstance) error {
|
||||
return ci.addArgs("--cuuid", ctrUUID)
|
||||
}
|
||||
}
|
||||
|
||||
func WithRuntimePath(path string) ConmonOption {
|
||||
return func(ci *ConmonInstance) error {
|
||||
return ci.addArgs("--runtime", path)
|
||||
}
|
||||
}
|
||||
|
||||
func WithLogDriver(driver, path string) ConmonOption {
|
||||
return func(ci *ConmonInstance) error {
|
||||
fullDriver := path
|
||||
if driver != "" {
|
||||
fullDriver = fmt.Sprintf("%s:%s", driver, path)
|
||||
}
|
||||
return ci.addArgs("--log-path", fullDriver)
|
||||
}
|
||||
}
|
||||
|
||||
func WithLogPath(path string) ConmonOption {
|
||||
return func(ci *ConmonInstance) error {
|
||||
return ci.addArgs("--log-path", path)
|
||||
}
|
||||
}
|
||||
|
||||
func WithBundlePath(path string) ConmonOption {
|
||||
return func(ci *ConmonInstance) error {
|
||||
return ci.addArgs("--bundle", path)
|
||||
}
|
||||
}
|
||||
|
||||
func WithSyslog() ConmonOption {
|
||||
return func(ci *ConmonInstance) error {
|
||||
return ci.addArgs("--syslog")
|
||||
}
|
||||
}
|
||||
|
||||
func WithLogLevel(level string) ConmonOption {
|
||||
return func(ci *ConmonInstance) error {
|
||||
// TODO verify level is right
|
||||
return ci.addArgs("--log-level", level)
|
||||
}
|
||||
}
|
||||
|
||||
func WithSocketPath(path string) ConmonOption {
|
||||
return func(ci *ConmonInstance) error {
|
||||
// TODO verify path is right
|
||||
// TODO automatically add container ID? right now it's callers responsibility
|
||||
return ci.addArgs("--socket-dir-path", path)
|
||||
}
|
||||
}
|
||||
|
||||
func WithContainerPidFile(path string) ConmonOption {
|
||||
return func(ci *ConmonInstance) error {
|
||||
// TODO verify path is right
|
||||
return ci.addArgs("--container-pidfile", path)
|
||||
}
|
||||
}
|
||||
|
||||
func WithRuntimeConfig(path string) ConmonOption {
|
||||
return func(ci *ConmonInstance) error {
|
||||
// TODO verify path is right
|
||||
return ci.addArgs("--container-pidfile", path)
|
||||
}
|
||||
}
|
||||
|
||||
func WithConmonPidFile(path string) ConmonOption {
|
||||
return func(ci *ConmonInstance) error {
|
||||
// TODO verify path is right
|
||||
ci.pidFile = path
|
||||
return ci.addArgs("--conmon-pidfile", path)
|
||||
}
|
||||
}
|
||||
|
||||
func WithStartPipe() ConmonOption {
|
||||
return func(ci *ConmonInstance) error {
|
||||
read, write, err := newPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ci.parentStartPipe = write
|
||||
ci.childStartPipe = read
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithAttachPipe() ConmonOption {
|
||||
return func(ci *ConmonInstance) error {
|
||||
read, write, err := newPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ci.parentAttachPipe = read
|
||||
ci.childAttachPipe = write
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithSyncPipe() ConmonOption {
|
||||
return func(ci *ConmonInstance) error {
|
||||
read, write, err := newPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ci.parentSyncPipe = read
|
||||
ci.childSyncPipe = write
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// newPipe creates a unix socket pair for communication
|
||||
func newPipe() (read *os.File, write *os.File, err error) {
|
||||
fds, err := unix.Socketpair(unix.AF_LOCAL, unix.SOCK_SEQPACKET|unix.SOCK_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return os.NewFile(uintptr(fds[1]), "read"), os.NewFile(uintptr(fds[0]), "write"), nil
|
||||
}
|
||||
|
||||
func (ci *ConmonInstance) addArgs(args ...string) error {
|
||||
ci.args = append(ci.args, args...)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
package conmon
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containers/podman/v4/libpod/define"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (ci *ConmonInstance) configurePipeEnv() error {
|
||||
if ci.cmd == nil {
|
||||
return errors.Errorf("conmon instance command must be configured")
|
||||
}
|
||||
if ci.started {
|
||||
return errors.Errorf("conmon instance environment cannot be configured after it's started")
|
||||
}
|
||||
// TODO handle PreserveFDs
|
||||
preserveFDs := 0
|
||||
fdCount := 3
|
||||
if ci.childSyncPipe != nil {
|
||||
ci.cmd.Env = append(ci.cmd.Env, fmt.Sprintf("_OCI_SYNCPIPE=%d", preserveFDs+fdCount))
|
||||
ci.cmd.ExtraFiles = append(ci.cmd.ExtraFiles, ci.childSyncPipe)
|
||||
fdCount++
|
||||
}
|
||||
if ci.childStartPipe != nil {
|
||||
ci.cmd.Env = append(ci.cmd.Env, fmt.Sprintf("_OCI_STARTPIPE=%d", preserveFDs+fdCount))
|
||||
ci.cmd.ExtraFiles = append(ci.cmd.ExtraFiles, ci.childStartPipe)
|
||||
fdCount++
|
||||
}
|
||||
if ci.childAttachPipe != nil {
|
||||
ci.cmd.Env = append(ci.cmd.Env, fmt.Sprintf("_OCI_ATTACHPIPE=%d", preserveFDs+fdCount))
|
||||
ci.cmd.ExtraFiles = append(ci.cmd.ExtraFiles, ci.childAttachPipe)
|
||||
fdCount++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ci *ConmonInstance) ContainerExitCode() (int, error) {
|
||||
return readConmonPipeData(ci.parentSyncPipe)
|
||||
}
|
||||
|
||||
// readConmonPipeData attempts to read a syncInfo struct from the pipe
|
||||
// TODO podman checks for ociLog capability
|
||||
func readConmonPipeData(pipe *os.File) (int, error) {
|
||||
// syncInfo is used to return data from monitor process to daemon
|
||||
type syncInfo struct {
|
||||
Data int `json:"data"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// Wait to get container pid from conmon
|
||||
type syncStruct struct {
|
||||
si *syncInfo
|
||||
err error
|
||||
}
|
||||
ch := make(chan syncStruct)
|
||||
go func() {
|
||||
var si *syncInfo
|
||||
rdr := bufio.NewReader(pipe)
|
||||
b, err := rdr.ReadBytes('\n')
|
||||
if err != nil {
|
||||
ch <- syncStruct{err: err}
|
||||
}
|
||||
if err := json.Unmarshal(b, &si); err != nil {
|
||||
ch <- syncStruct{err: err}
|
||||
return
|
||||
}
|
||||
ch <- syncStruct{si: si}
|
||||
}()
|
||||
|
||||
data := -1
|
||||
select {
|
||||
case ss := <-ch:
|
||||
if ss.err != nil {
|
||||
return -1, errors.Wrapf(ss.err, "error received on processing data from conmon pipe")
|
||||
}
|
||||
if ss.si.Data < 0 {
|
||||
if ss.si.Message != "" {
|
||||
return ss.si.Data, getOCIRuntimeError(ss.si.Message)
|
||||
}
|
||||
return ss.si.Data, errors.Wrapf(define.ErrInternal, "conmon invocation failed")
|
||||
}
|
||||
data = ss.si.Data
|
||||
case <-time.After(1 * time.Minute):
|
||||
return -1, errors.Wrapf(define.ErrInternal, "conmon invocation timeout")
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func getOCIRuntimeError(runtimeMsg string) error {
|
||||
// TODO base off of log level
|
||||
// includeFullOutput := logrus.GetLevel() == logrus.DebugLevel
|
||||
includeFullOutput := true
|
||||
|
||||
if match := regexp.MustCompile("(?i).*permission denied.*|.*operation not permitted.*").FindString(runtimeMsg); match != "" {
|
||||
errStr := match
|
||||
if includeFullOutput {
|
||||
errStr = runtimeMsg
|
||||
}
|
||||
return errors.Wrapf(define.ErrOCIRuntimePermissionDenied, "%s", strings.Trim(errStr, "\n"))
|
||||
}
|
||||
if match := regexp.MustCompile("(?i).*executable file not found in.*|.*no such file or directory.*").FindString(runtimeMsg); match != "" {
|
||||
errStr := match
|
||||
if includeFullOutput {
|
||||
errStr = runtimeMsg
|
||||
}
|
||||
return errors.Wrapf(define.ErrOCIRuntimeNotFound, "%s", strings.Trim(errStr, "\n"))
|
||||
}
|
||||
return errors.Wrapf(define.ErrOCIRuntime, "%s", strings.Trim(runtimeMsg, "\n"))
|
||||
}
|
||||
|
||||
// writeConmonPipeData writes data to a pipe. The actual content does not matter
|
||||
// as it is used as a signal for conmon to stop blocking on a read
|
||||
func writeConmonPipeData(pipe *os.File) error {
|
||||
someData := []byte{0}
|
||||
_, err := pipe.Write(someData)
|
||||
return err
|
||||
}
|
||||
|
||||
func (ci *ConmonInstance) closePipesOnCleanup() {
|
||||
ci.parentSyncPipe.Close()
|
||||
ci.parentStartPipe.Close()
|
||||
ci.parentAttachPipe.Close()
|
||||
}
|
||||
|
|
@ -1,344 +0,0 @@
|
|||
package conmon_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/containers/conmon/runner/conmon"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var _ = Describe("conmon", func() {
|
||||
Describe("version", func() {
|
||||
It("Should return conmon version", func() {
|
||||
out, _ := getConmonOutputGivenOptions(
|
||||
conmon.WithVersion(),
|
||||
conmon.WithPath(conmonPath),
|
||||
)
|
||||
Expect(out).To(ContainSubstring("conmon version"))
|
||||
Expect(out).To(ContainSubstring("commit"))
|
||||
})
|
||||
})
|
||||
Describe("no container ID", func() {
|
||||
It("should fail", func() {
|
||||
_, err := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
)
|
||||
Expect(err).To(ContainSubstring("conmon: Container ID not provided. Use --cid"))
|
||||
})
|
||||
})
|
||||
Describe("no container UUID", func() {
|
||||
It("should fail", func() {
|
||||
_, err := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
)
|
||||
Expect(err).To(ContainSubstring("Container UUID not provided. Use --cuuid"))
|
||||
})
|
||||
})
|
||||
Describe("runtime path", func() {
|
||||
It("no path should fail", func() {
|
||||
_, err := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
)
|
||||
Expect(err).To(ContainSubstring("Runtime path not provided. Use --runtime"))
|
||||
})
|
||||
It("invalid path should fail", func() {
|
||||
_, err := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(invalidPath),
|
||||
)
|
||||
Expect(err).To(ContainSubstring(fmt.Sprintf("Runtime path %s is not valid", invalidPath)))
|
||||
})
|
||||
})
|
||||
Describe("ctr logs", func() {
|
||||
var tmpDir string
|
||||
var tmpLogPath string
|
||||
var origCwd string
|
||||
BeforeEach(func() {
|
||||
d, err := ioutil.TempDir(os.TempDir(), "conmon-")
|
||||
Expect(err).To(BeNil())
|
||||
tmpDir = d
|
||||
tmpLogPath = filepath.Join(tmpDir, "log")
|
||||
origCwd, err = os.Getwd()
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
AfterEach(func() {
|
||||
for {
|
||||
// There is a race condition on the directory deletion
|
||||
// as conmon could still be running and creating files
|
||||
// under tmpDir. Attempt rmdir again if it fails with
|
||||
// ENOTEMPTY.
|
||||
err := os.RemoveAll(tmpDir)
|
||||
if err != nil && errors.Is(err, unix.ENOTEMPTY) {
|
||||
continue
|
||||
}
|
||||
Expect(err).To(BeNil())
|
||||
break
|
||||
}
|
||||
Expect(os.RemoveAll(tmpDir)).To(BeNil())
|
||||
err := os.Chdir(origCwd)
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
It("no log driver should fail", func() {
|
||||
_, stderr := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(validPath),
|
||||
)
|
||||
Expect(stderr).To(ContainSubstring("Log driver not provided. Use --log-path"))
|
||||
})
|
||||
It("empty log driver should fail", func() {
|
||||
_, stderr := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(validPath),
|
||||
conmon.WithLogPath(""),
|
||||
)
|
||||
Expect(stderr).To(ContainSubstring("log-path must not be empty"))
|
||||
})
|
||||
It("empty log driver and path should fail", func() {
|
||||
_, stderr := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(validPath),
|
||||
conmon.WithLogPath(":"),
|
||||
)
|
||||
Expect(stderr).To(ContainSubstring("log-path must not be empty"))
|
||||
})
|
||||
It("k8s-file requires a filename", func() {
|
||||
_, stderr := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(validPath),
|
||||
conmon.WithLogPath("k8s-file"),
|
||||
)
|
||||
Expect(stderr).To(ContainSubstring("k8s-file requires a filename"))
|
||||
})
|
||||
It("k8s-file: requires a filename", func() {
|
||||
_, stderr := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(validPath),
|
||||
conmon.WithLogPath("k8s-file:"),
|
||||
)
|
||||
Expect(stderr).To(ContainSubstring("k8s-file requires a filename"))
|
||||
})
|
||||
It("log driver as path should pass", func() {
|
||||
_, stderr := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(validPath),
|
||||
conmon.WithLogDriver("", tmpLogPath),
|
||||
)
|
||||
Expect(stderr).To(BeEmpty())
|
||||
|
||||
_, err := os.Stat(tmpLogPath)
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
It("log driver as k8s-file:path should pass", func() {
|
||||
_, stderr := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(validPath),
|
||||
conmon.WithLogDriver("k8s-file", tmpLogPath),
|
||||
)
|
||||
Expect(stderr).To(BeEmpty())
|
||||
|
||||
_, err := os.Stat(tmpLogPath)
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
It("log driver as :path should pass", func() {
|
||||
_, stderr := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(validPath),
|
||||
conmon.WithLogPath(":"+tmpLogPath),
|
||||
)
|
||||
Expect(stderr).To(BeEmpty())
|
||||
|
||||
_, err := os.Stat(tmpLogPath)
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
It("log driver as none should pass", func() {
|
||||
direrr := os.Chdir(tmpDir)
|
||||
Expect(direrr).To(BeNil())
|
||||
|
||||
_, stderr := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(validPath),
|
||||
conmon.WithLogDriver("none", ""),
|
||||
)
|
||||
Expect(stderr).To(BeEmpty())
|
||||
|
||||
_, err := os.Stat("none")
|
||||
Expect(err).NotTo(BeNil())
|
||||
})
|
||||
It("log driver as off should pass", func() {
|
||||
direrr := os.Chdir(tmpDir)
|
||||
Expect(direrr).To(BeNil())
|
||||
|
||||
_, stderr := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(validPath),
|
||||
conmon.WithLogDriver("off", ""),
|
||||
)
|
||||
Expect(stderr).To(BeEmpty())
|
||||
|
||||
_, err := os.Stat("off")
|
||||
Expect(err).NotTo(BeNil())
|
||||
})
|
||||
It("log driver as null should pass", func() {
|
||||
direrr := os.Chdir(tmpDir)
|
||||
Expect(direrr).To(BeNil())
|
||||
|
||||
_, stderr := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(validPath),
|
||||
conmon.WithLogDriver("null", ""),
|
||||
)
|
||||
Expect(stderr).To(BeEmpty())
|
||||
|
||||
_, err := os.Stat("none")
|
||||
Expect(err).NotTo(BeNil())
|
||||
})
|
||||
It("log driver as journald should pass", func() {
|
||||
direrr := os.Chdir(tmpDir)
|
||||
Expect(direrr).To(BeNil())
|
||||
|
||||
_, stderr := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(validPath),
|
||||
conmon.WithLogDriver("journald", ""),
|
||||
)
|
||||
Expect(stderr).To(BeEmpty())
|
||||
|
||||
_, err := os.Stat("journald")
|
||||
Expect(err).NotTo(BeNil())
|
||||
})
|
||||
It("log driver as :journald should pass", func() {
|
||||
direrr := os.Chdir(tmpDir)
|
||||
Expect(direrr).To(BeNil())
|
||||
|
||||
_, stderr := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(validPath),
|
||||
conmon.WithLogPath(":journald"),
|
||||
)
|
||||
Expect(stderr).To(BeEmpty())
|
||||
|
||||
_, err := os.Stat("journald")
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
It("log driver as journald with short cid should fail", func() {
|
||||
// conmon requires a cid of len > 12
|
||||
shortCtrID := "abcdefghijkl"
|
||||
_, stderr := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(shortCtrID),
|
||||
conmon.WithContainerUUID(shortCtrID),
|
||||
conmon.WithRuntimePath(validPath),
|
||||
conmon.WithLogDriver("journald", ""),
|
||||
)
|
||||
Expect(stderr).To(ContainSubstring("Container ID must be longer than 12 characters"))
|
||||
})
|
||||
It("log driver as k8s-file with path should pass", func() {
|
||||
_, stderr := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(validPath),
|
||||
conmon.WithLogDriver("k8s-file", tmpLogPath),
|
||||
)
|
||||
Expect(stderr).To(BeEmpty())
|
||||
|
||||
_, err := os.Stat(tmpLogPath)
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
It("log driver as k8s-file with invalid path should fail", func() {
|
||||
_, stderr := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(validPath),
|
||||
conmon.WithLogDriver("k8s-file", invalidPath),
|
||||
)
|
||||
Expect(stderr).To(ContainSubstring("Failed to open log file"))
|
||||
})
|
||||
It("log driver as invalid driver should fail", func() {
|
||||
invalidLogDriver := "invalid"
|
||||
_, stderr := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(validPath),
|
||||
conmon.WithLogDriver(invalidLogDriver, tmpLogPath),
|
||||
)
|
||||
Expect(stderr).To(ContainSubstring("No such log driver " + invalidLogDriver))
|
||||
})
|
||||
It("log driver as invalid driver with a blank path should fail", func() {
|
||||
invalidLogDriver := "invalid"
|
||||
_, stderr := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(validPath),
|
||||
conmon.WithLogDriver(invalidLogDriver, ""),
|
||||
)
|
||||
Expect(stderr).To(ContainSubstring("No such log driver " + invalidLogDriver))
|
||||
})
|
||||
It("multiple log drivers should pass", func() {
|
||||
_, stderr := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(validPath),
|
||||
conmon.WithLogDriver("k8s-file", tmpLogPath),
|
||||
conmon.WithLogDriver("journald", ""),
|
||||
)
|
||||
Expect(stderr).To(BeEmpty())
|
||||
|
||||
_, err := os.Stat(tmpLogPath)
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
It("multiple log drivers with one invalid should fail", func() {
|
||||
invalidLogDriver := "invalid"
|
||||
_, stderr := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(validPath),
|
||||
conmon.WithLogDriver("k8s-file", tmpLogPath),
|
||||
conmon.WithLogDriver(invalidLogDriver, tmpLogPath),
|
||||
)
|
||||
Expect(stderr).To(ContainSubstring("No such log driver " + invalidLogDriver))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
package conmon_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/containers/conmon/runner/conmon"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("conmon ctr logs", func() {
|
||||
var tmpDir string
|
||||
var tmpLogPath string
|
||||
const invalidLogDriver = "invalid"
|
||||
BeforeEach(func() {
|
||||
d, err := ioutil.TempDir(os.TempDir(), "conmon-")
|
||||
Expect(err).To(BeNil())
|
||||
tmpDir = d
|
||||
tmpLogPath = filepath.Join(tmpDir, "log")
|
||||
})
|
||||
AfterEach(func() {
|
||||
Expect(os.RemoveAll(tmpDir)).To(BeNil())
|
||||
})
|
||||
It("no log driver should fail", func() {
|
||||
_, stderr := getConmonOutputGivenLogOpts()
|
||||
Expect(stderr).To(ContainSubstring("Log driver not provided. Use --log-path"))
|
||||
})
|
||||
It("log driver as path should pass", func() {
|
||||
_, stderr := getConmonOutputGivenLogOpts(conmon.WithLogDriver("", tmpLogPath))
|
||||
Expect(stderr).To(BeEmpty())
|
||||
|
||||
_, err := os.Stat(tmpLogPath)
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
It("log driver as journald should pass", func() {
|
||||
_, stderr := getConmonOutputGivenLogOpts(conmon.WithLogDriver("journald", ""))
|
||||
Expect(stderr).To(BeEmpty())
|
||||
})
|
||||
It("log driver as journald with short cid should fail", func() {
|
||||
// conmon requires a cid of len > 12
|
||||
shortCtrID := "abcdefghijkl"
|
||||
|
||||
_, stderr := getConmonOutputGivenLogOpts(
|
||||
conmon.WithLogDriver("journald", ""),
|
||||
conmon.WithContainerID(shortCtrID),
|
||||
)
|
||||
Expect(stderr).To(ContainSubstring("Container ID must be longer than 12 characters"))
|
||||
})
|
||||
It("log driver as k8s-file with path should pass", func() {
|
||||
_, stderr := getConmonOutputGivenLogOpts(conmon.WithLogDriver("k8s-file", tmpLogPath))
|
||||
Expect(stderr).To(BeEmpty())
|
||||
|
||||
_, err := os.Stat(tmpLogPath)
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
It("log driver as passthrough should pass", func() {
|
||||
stdout, stderr := getConmonOutputGivenLogOpts(conmon.WithLogDriver("passthrough", ""))
|
||||
Expect(stdout).To(BeEmpty())
|
||||
Expect(stderr).To(BeEmpty())
|
||||
})
|
||||
It("log driver as k8s-file with invalid path should fail", func() {
|
||||
_, stderr := getConmonOutputGivenLogOpts(conmon.WithLogDriver("k8s-file", invalidPath))
|
||||
Expect(stderr).To(ContainSubstring("Failed to open log file"))
|
||||
})
|
||||
It("log driver as invalid driver should fail", func() {
|
||||
_, stderr := getConmonOutputGivenLogOpts(conmon.WithLogDriver(invalidLogDriver, tmpLogPath))
|
||||
Expect(stderr).To(ContainSubstring("No such log driver " + invalidLogDriver))
|
||||
})
|
||||
It("multiple log drivers should pass", func() {
|
||||
_, stderr := getConmonOutputGivenLogOpts(
|
||||
conmon.WithLogDriver("k8s-file", tmpLogPath),
|
||||
conmon.WithLogDriver("journald", ""),
|
||||
)
|
||||
Expect(stderr).To(BeEmpty())
|
||||
|
||||
_, err := os.Stat(tmpLogPath)
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
It("multiple log drivers with one invalid should fail", func() {
|
||||
_, stderr := getConmonOutputGivenLogOpts(
|
||||
conmon.WithLogDriver("k8s-file", tmpLogPath),
|
||||
conmon.WithLogDriver(invalidLogDriver, tmpLogPath),
|
||||
)
|
||||
Expect(stderr).To(ContainSubstring("No such log driver " + invalidLogDriver))
|
||||
})
|
||||
})
|
||||
|
||||
func getConmonOutputGivenLogOpts(logDriverOpts ...conmon.ConmonOption) (string, string) {
|
||||
opts := []conmon.ConmonOption{
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(validPath),
|
||||
}
|
||||
opts = append(opts, logDriverOpts...)
|
||||
return getConmonOutputGivenOptions(opts...)
|
||||
}
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
package conmon_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/containers/conmon/runner/conmon"
|
||||
"github.com/containers/storage/pkg/stringid"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/opencontainers/runtime-tools/generate"
|
||||
)
|
||||
|
||||
var _ = Describe("runc", func() {
|
||||
var (
|
||||
tmpDir string
|
||||
tmpLogPath string
|
||||
tmpPidFile string
|
||||
tmpRootfs string
|
||||
)
|
||||
BeforeEach(func() {
|
||||
// save busy box binary if we don't have it
|
||||
Expect(cacheBusyBox()).To(BeNil())
|
||||
|
||||
// create tmpDir
|
||||
d, err := ioutil.TempDir(os.TempDir(), "conmon-")
|
||||
Expect(err).To(BeNil())
|
||||
tmpDir = d
|
||||
|
||||
// generate logging path
|
||||
tmpLogPath = filepath.Join(tmpDir, "log")
|
||||
|
||||
// generate container ID
|
||||
ctrID = stringid.GenerateNonCryptoID()
|
||||
|
||||
// create the rootfs of the "container"
|
||||
tmpRootfs = filepath.Join(tmpDir, "rootfs")
|
||||
Expect(os.MkdirAll(tmpRootfs, 0755)).To(BeNil())
|
||||
|
||||
tmpPidFile = filepath.Join(tmpDir, "pidfile")
|
||||
|
||||
Expect(os.Link(busyboxDest, filepath.Join(tmpRootfs, "busybox"))).To(BeNil())
|
||||
|
||||
// finally, create config.json
|
||||
_, err = generateRuntimeConfig(tmpDir, tmpRootfs)
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
AfterEach(func() {
|
||||
Expect(os.RemoveAll(tmpDir)).To(BeNil())
|
||||
Expect(runRuntimeCommand("delete", "-f", ctrID)).To(BeNil())
|
||||
})
|
||||
It("simple runtime test", func() {
|
||||
stdout, stderr := getConmonOutputGivenOptions(
|
||||
conmon.WithPath(conmonPath),
|
||||
conmon.WithContainerID(ctrID),
|
||||
conmon.WithContainerUUID(ctrID),
|
||||
conmon.WithRuntimePath(runtimePath),
|
||||
conmon.WithLogDriver("k8s-file", tmpLogPath),
|
||||
conmon.WithBundlePath(tmpDir),
|
||||
conmon.WithSocketPath(tmpDir),
|
||||
conmon.WithSyslog(),
|
||||
conmon.WithLogLevel("trace"),
|
||||
conmon.WithContainerPidFile(tmpPidFile),
|
||||
conmon.WithConmonPidFile(fmt.Sprintf("%s/conmon-pidfile", tmpDir)),
|
||||
conmon.WithSyncPipe(),
|
||||
)
|
||||
Expect(stdout).To(BeEmpty())
|
||||
Expect(stderr).To(BeEmpty())
|
||||
|
||||
Expect(runRuntimeCommand("start", ctrID)).To(BeNil())
|
||||
|
||||
Expect(getFileContents(tmpLogPath)).To(ContainSubstring("busybox"))
|
||||
Expect(getFileContents(tmpPidFile)).To(Not(BeEmpty()))
|
||||
})
|
||||
})
|
||||
|
||||
func getFileContents(filename string) string {
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
Expect(err).To(BeNil())
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func generateRuntimeConfig(bundlePath, rootfs string) (string, error) {
|
||||
configPath := filepath.Join(bundlePath, "config.json")
|
||||
g, err := generate.New("linux")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
g.SetProcessCwd("/")
|
||||
g.SetProcessArgs([]string{"/busybox", "ls"})
|
||||
g.SetRootPath(rootfs)
|
||||
|
||||
if err := g.SaveToFile(configPath, generate.ExportOptions{}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return configPath, nil
|
||||
}
|
||||
|
|
@ -1,191 +0,0 @@
|
|||
package conmon_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/containers/conmon/runner/conmon"
|
||||
"github.com/coreos/go-systemd/v22/sdjournal"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var (
|
||||
conmonPath = "/usr/bin/conmon"
|
||||
runtimePath = "/usr/bin/runc"
|
||||
busyboxSource = "https://busybox.net/downloads/binaries/1.31.0-i686-uclibc/busybox"
|
||||
busyboxDestDir = "/tmp/conmon-test-images"
|
||||
busyboxDest = "/tmp/conmon-test-images/busybox"
|
||||
ctrID = "abcdefghijklm"
|
||||
validPath = "/tmp"
|
||||
invalidPath = "/not/a/path"
|
||||
skopeoPath = "/usr/bin/skopeo"
|
||||
)
|
||||
|
||||
func TestConmon(t *testing.T) {
|
||||
configureSuiteFromEnv()
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Conmon Suite")
|
||||
}
|
||||
|
||||
func getConmonOutputGivenOptions(options ...conmon.ConmonOption) (string, string) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
var stdin bytes.Buffer
|
||||
|
||||
options = append(options, conmon.WithStdout(&stdout), conmon.WithStderr(&stderr), conmon.WithStdin(&stdin))
|
||||
|
||||
ci, err := conmon.CreateAndExecConmon(options...)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
defer ci.Cleanup()
|
||||
|
||||
ci.Wait()
|
||||
|
||||
pid, _ := ci.Pid()
|
||||
if pid < 0 {
|
||||
return stdout.String(), stderr.String()
|
||||
}
|
||||
|
||||
_, err = ci.ContainerExitCode()
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
journalerr, err := getConmonJournalOutput(pid, 3)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
alljournalout, err := getConmonJournalOutput(pid, -1)
|
||||
Expect(err).To(BeNil())
|
||||
fmt.Fprintf(GinkgoWriter, alljournalout+"\n")
|
||||
|
||||
return stdout.String(), stderr.String() + journalerr
|
||||
}
|
||||
|
||||
func getConmonJournalOutput(pid int, level int) (string, error) {
|
||||
matches := []sdjournal.Match{
|
||||
{
|
||||
Field: sdjournal.SD_JOURNAL_FIELD_COMM,
|
||||
Value: "conmon",
|
||||
},
|
||||
{
|
||||
Field: sdjournal.SD_JOURNAL_FIELD_PID,
|
||||
Value: strconv.Itoa(pid),
|
||||
},
|
||||
}
|
||||
if level > 0 {
|
||||
matches = append(matches, sdjournal.Match{
|
||||
Field: sdjournal.SD_JOURNAL_FIELD_PRIORITY,
|
||||
Value: strconv.Itoa(level),
|
||||
})
|
||||
}
|
||||
r, err := sdjournal.NewJournalReader(sdjournal.JournalReaderConfig{
|
||||
Matches: matches,
|
||||
Formatter: formatter,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
return readAllFromBuffer(r)
|
||||
}
|
||||
|
||||
func formatter(entry *sdjournal.JournalEntry) (string, error) {
|
||||
return entry.Fields[sdjournal.SD_JOURNAL_FIELD_MESSAGE], nil
|
||||
}
|
||||
|
||||
func readAllFromBuffer(r io.ReadCloser) (string, error) {
|
||||
bufLen := 16384
|
||||
stringOutput := ""
|
||||
|
||||
bytes := make([]byte, bufLen)
|
||||
// /me complains about no do-while in go
|
||||
ec, err := r.Read(bytes)
|
||||
for ec != 0 && err == nil {
|
||||
// because we are reusing bytes, we need to make
|
||||
// sure the old data doesn't get into the new line
|
||||
bytestr := string(bytes[:ec])
|
||||
stringOutput += string(bytestr)
|
||||
ec, err = r.Read(bytes)
|
||||
}
|
||||
if err != nil && err != io.EOF {
|
||||
return stringOutput, err
|
||||
}
|
||||
return stringOutput, nil
|
||||
}
|
||||
|
||||
func configureSuiteFromEnv() {
|
||||
if path := os.Getenv("CONMON_BINARY"); path != "" {
|
||||
conmonPath = path
|
||||
}
|
||||
if path := os.Getenv("RUNTIME_BINARY"); path != "" {
|
||||
runtimePath = path
|
||||
}
|
||||
}
|
||||
|
||||
func cacheBusyBox() error {
|
||||
if _, err := os.Stat(busyboxDest); err == nil {
|
||||
return nil
|
||||
}
|
||||
if err := os.MkdirAll(busyboxDestDir, 0755); err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
if err := downloadFile(busyboxSource, busyboxDest); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chmod(busyboxDest, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// source: https://progolang.com/how-to-download-files-in-go/
|
||||
// downloadFile will download a url and store it in local filepath.
|
||||
// It writes to the destination file as it downloads it, without
|
||||
// loading the entire file into memory.
|
||||
func downloadFile(url string, filepath string) error {
|
||||
// Create the file
|
||||
out, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Get the data
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Write the body to file
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runRuntimeCommand(args ...string) error {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
cmd := exec.Command(runtimePath, args...)
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Run()
|
||||
stdoutString := stdout.String()
|
||||
if stdoutString != "" {
|
||||
fmt.Fprintf(GinkgoWriter, stdoutString+"\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
41
src/cgroup.c
41
src/cgroup.c
|
|
@ -33,7 +33,8 @@ static void setup_oom_handling_cgroup_v2(int pid);
|
|||
static void setup_oom_handling_cgroup_v1(int pid);
|
||||
static gboolean oom_cb_cgroup_v2(int fd, GIOCondition condition, G_GNUC_UNUSED gpointer user_data);
|
||||
static gboolean oom_cb_cgroup_v1(int fd, GIOCondition condition, G_GNUC_UNUSED gpointer user_data);
|
||||
static int write_oom_files();
|
||||
static int create_oom_files();
|
||||
static int create_oom_file(const char *base_path);
|
||||
|
||||
void setup_oom_handling(int pid)
|
||||
{
|
||||
|
|
@ -261,7 +262,7 @@ static gboolean oom_cb_cgroup_v1(int fd, GIOCondition condition, gpointer user_d
|
|||
|
||||
/* we catch the two other cases here, both of which are OOM kill events */
|
||||
ninfo("OOM event received");
|
||||
write_oom_files();
|
||||
create_oom_files();
|
||||
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
|
@ -306,7 +307,7 @@ gboolean check_cgroup2_oom()
|
|||
continue;
|
||||
|
||||
if (counter != last_counter) {
|
||||
if (write_oom_files() == 0)
|
||||
if (create_oom_files() == 0)
|
||||
last_counter = counter;
|
||||
}
|
||||
return G_SOURCE_CONTINUE;
|
||||
|
|
@ -314,25 +315,31 @@ gboolean check_cgroup2_oom()
|
|||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/* write the appropriate files to tell the caller there was an oom event
|
||||
* this can be used for v1 and v2 OOMS
|
||||
/* create the appropriate files to tell the caller there was an oom event
|
||||
* this can be used for v1 and v2 OOMs
|
||||
* returns 0 on success, negative value on failure
|
||||
*/
|
||||
static int write_oom_files()
|
||||
static int create_oom_files()
|
||||
{
|
||||
ninfo("OOM received");
|
||||
if (opt_persist_path) {
|
||||
_cleanup_free_ char *ctr_oom_file_path = g_build_filename(opt_persist_path, "oom", NULL);
|
||||
_cleanup_close_ int ctr_oom_fd = open(ctr_oom_file_path, O_CREAT | O_CLOEXEC, 0666);
|
||||
if (ctr_oom_fd < 0) {
|
||||
nwarn("Failed to write oom file");
|
||||
}
|
||||
int r = 0;
|
||||
r |= create_oom_file(opt_persist_path);
|
||||
r |= create_oom_file(opt_bundle_path);
|
||||
return r;
|
||||
}
|
||||
|
||||
static int create_oom_file(const char *base_path)
|
||||
{
|
||||
if (base_path == NULL || base_path[0] == '\0')
|
||||
return 0;
|
||||
|
||||
_cleanup_free_ char *ctr_oom_file_path = g_build_filename(base_path, "oom", NULL);
|
||||
_cleanup_close_ int ctr_oom_fd = open(ctr_oom_file_path, O_CREAT | O_CLOEXEC, 0666);
|
||||
if (ctr_oom_fd < 0) {
|
||||
nwarnf("Failed to write oom file to the %s path", base_path);
|
||||
return -1;
|
||||
}
|
||||
_cleanup_close_ int oom_fd = open("oom", O_CREAT | O_CLOEXEC, 0666);
|
||||
if (oom_fd < 0) {
|
||||
nwarn("Failed to write oom file");
|
||||
}
|
||||
return oom_fd >= 0 ? 0 : -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ int opt_exit_delay = 0;
|
|||
gboolean opt_replace_listen_pid = FALSE;
|
||||
char *opt_log_level = NULL;
|
||||
char *opt_log_tag = NULL;
|
||||
gchar **opt_log_labels = NULL;
|
||||
gboolean opt_sync = FALSE;
|
||||
gboolean opt_no_sync_log = FALSE;
|
||||
char *opt_sdnotify_socket = NULL;
|
||||
|
|
@ -78,6 +79,8 @@ GOptionEntry opt_entries[] = {
|
|||
{"log-size-max", 0, 0, G_OPTION_ARG_INT64, &opt_log_size_max, "Maximum size of log file", NULL},
|
||||
{"log-global-size-max", 0, 0, G_OPTION_ARG_INT64, &opt_log_global_size_max, "Maximum size of all log files", NULL},
|
||||
{"log-tag", 0, 0, G_OPTION_ARG_STRING, &opt_log_tag, "Additional tag to use for logging", NULL},
|
||||
{"log-label", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_log_labels,
|
||||
"Additional label to include in logs. Can be specified multiple times", NULL},
|
||||
{"name", 'n', 0, G_OPTION_ARG_STRING, &opt_name, "Container name", NULL},
|
||||
{"no-new-keyring", 0, 0, G_OPTION_ARG_NONE, &opt_no_new_keyring, "Do not create a new session keyring for the container", NULL},
|
||||
{"no-pivot", 0, 0, G_OPTION_ARG_NONE, &opt_no_pivot, "Do not use pivot_root", NULL},
|
||||
|
|
@ -194,5 +197,5 @@ void process_cli()
|
|||
if (opt_container_pid_file == NULL)
|
||||
opt_container_pid_file = g_strdup_printf("%s/pidfile-%s", cwd, opt_cid);
|
||||
|
||||
configure_log_drivers(opt_log_path, opt_log_size_max, opt_log_global_size_max, opt_cid, opt_name, opt_log_tag);
|
||||
configure_log_drivers(opt_log_path, opt_log_size_max, opt_log_global_size_max, opt_cid, opt_name, opt_log_tag, opt_log_labels);
|
||||
}
|
||||
|
|
|
|||
21
src/cmsg.c
21
src/cmsg.c
|
|
@ -36,19 +36,14 @@
|
|||
#define ECOMM EINVAL
|
||||
#endif
|
||||
|
||||
#define error(s) \
|
||||
#define errorf(fmt, ...) \
|
||||
do { \
|
||||
fprintf(stderr, "conmon: %s %s\n", s, strerror(errno)); \
|
||||
fprintf(stderr, "conmon: " fmt "\n", ##__VA_ARGS__); \
|
||||
errno = ECOMM; \
|
||||
goto err; /* return value */ \
|
||||
} while (0)
|
||||
|
||||
#define errorf(fmt, ...) \
|
||||
do { \
|
||||
fprintf(stderr, "conmon: " fmt ": %s\n", ##__VA_ARGS__, strerror(errno)); \
|
||||
errno = ECOMM; \
|
||||
goto err; /* return value */ \
|
||||
} while (0)
|
||||
#define error(s) errorf("%s", s)
|
||||
|
||||
/*
|
||||
* Sends a file descriptor along the sockfd provided. Returns the return
|
||||
|
|
@ -108,6 +103,7 @@ struct file_t recvfd(int sockfd)
|
|||
struct iovec iov[1] = {{0}};
|
||||
struct cmsghdr *cmsg;
|
||||
struct file_t file = {0};
|
||||
void *new_name;
|
||||
int *fdptr;
|
||||
int olderrno;
|
||||
|
||||
|
|
@ -117,7 +113,6 @@ struct file_t recvfd(int sockfd)
|
|||
} u;
|
||||
|
||||
/* Allocate a buffer. */
|
||||
/* TODO: Make this dynamic with MSG_PEEK. */
|
||||
file.name = malloc(TAG_BUFFER);
|
||||
if (!file.name)
|
||||
error("recvfd: failed to allocate file.tag buffer");
|
||||
|
|
@ -128,7 +123,7 @@ struct file_t recvfd(int sockfd)
|
|||
* See unix(7) and other well-hidden documentation.
|
||||
*/
|
||||
iov[0].iov_base = file.name;
|
||||
iov[0].iov_len = TAG_BUFFER;
|
||||
iov[0].iov_len = TAG_BUFFER - 1;
|
||||
|
||||
msg.msg_name = NULL;
|
||||
msg.msg_namelen = 0;
|
||||
|
|
@ -140,6 +135,12 @@ struct file_t recvfd(int sockfd)
|
|||
ssize_t ret = recvmsg(sockfd, &msg, 0);
|
||||
if (ret < 0)
|
||||
goto err;
|
||||
file.name[ret] = '\0';
|
||||
|
||||
/* Shrink the buffer to what is effectively used. */
|
||||
new_name = realloc(file.name, ret + 1);
|
||||
if (new_name)
|
||||
file.name = new_name;
|
||||
|
||||
cmsg = CMSG_FIRSTHDR(&msg);
|
||||
if (!cmsg)
|
||||
|
|
|
|||
|
|
@ -93,8 +93,8 @@ int main(int argc, char *argv[])
|
|||
pexit("Failed to fork the create command");
|
||||
} else if (main_pid != 0) {
|
||||
if (opt_conmon_pid_file) {
|
||||
char content[12];
|
||||
sprintf(content, "%i", main_pid);
|
||||
char content[16];
|
||||
snprintf(content, sizeof(content), "%i", main_pid);
|
||||
|
||||
if (!g_file_set_contents(opt_conmon_pid_file, content, strlen(content), &err)) {
|
||||
_pexitf("Failed to write conmon pidfile: %s", err->message);
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ static void bind_relative_to_dir(int dir_fd, int sock_fd, const char *path)
|
|||
if (dir_fd == -1) {
|
||||
strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);
|
||||
} else {
|
||||
snprintf(addr.sun_path, sizeof(addr.sun_path) - 1, "/proc/self/fd/%d/%s", dir_fd, path);
|
||||
snprintf(addr.sun_path, sizeof(addr.sun_path), "/proc/self/fd/%d/%s", dir_fd, path);
|
||||
}
|
||||
ndebugf("addr{sun_family=AF_UNIX, sun_path=%s}", addr.sun_path);
|
||||
|
||||
|
|
@ -137,6 +137,7 @@ static void bind_relative_to_dir(int dir_fd, int sock_fd, const char *path)
|
|||
|
||||
addr.sun_family = AF_UNIX;
|
||||
strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);
|
||||
addr.sun_path[sizeof(addr.sun_path) - 1] = '\0';
|
||||
ndebugf("addr{sun_family=AF_UNIX, sun_path=%s}", addr.sun_path);
|
||||
if (bindat(dir_fd, sock_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
|
||||
pexit("Failed to bind to console-socket");
|
||||
|
|
@ -239,6 +240,7 @@ void setup_notify_socket(char *socket_path)
|
|||
}
|
||||
local_notify_host_addr.sun_family = AF_UNIX;
|
||||
strncpy(local_notify_host_addr.sun_path, socket_path, sizeof(local_notify_host_addr.sun_path) - 1);
|
||||
local_notify_host_addr.sun_path[sizeof(local_notify_host_addr.sun_path) - 1] = '\0';
|
||||
}
|
||||
|
||||
/* No _cleanup_free_ here so we don't get a warning about unused variables
|
||||
|
|
@ -558,13 +560,17 @@ static void sock_try_write_to_local_sock(struct remote_sock_s *sock)
|
|||
|
||||
if (local_sock->is_stream) {
|
||||
w = write(*(local_sock->fd), sock->buf + sock->off, sock->remaining);
|
||||
if (w < 0) {
|
||||
pwarnf("Failed to write to fd %s", local_sock->label);
|
||||
}
|
||||
} else {
|
||||
w = sendto(*(local_sock->fd), sock->buf + sock->off, sock->remaining, MSG_DONTWAIT | MSG_NOSIGNAL,
|
||||
(struct sockaddr *)local_sock->addr, sizeof(*(local_sock->addr)));
|
||||
if (w < 0) {
|
||||
pwarnf("Failed to write to socket %s", local_sock->label);
|
||||
}
|
||||
}
|
||||
if (w < 0) {
|
||||
nwarnf("Failed to write %s", local_sock->label);
|
||||
} else {
|
||||
if (w > 0) {
|
||||
sock->off += w;
|
||||
sock->remaining -= w;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,25 @@ static void check_child_processes(GHashTable *pid_to_handler, GHashTable *cache)
|
|||
continue;
|
||||
|
||||
if (pid < 0 && errno == ECHILD) {
|
||||
/* Before quitting, check if container_pid is still alive.
|
||||
* In some systemd configurations, the container process may not be
|
||||
* a direct child, so we won't receive SIGCHLD when it exits.
|
||||
* Use kill(pid, 0) to check if the process still exists. */
|
||||
if (container_pid > 0) {
|
||||
if (kill(container_pid, 0) == 0) {
|
||||
/* Container process is still alive but not our child.
|
||||
* Don't quit the main loop yet. */
|
||||
ninfof("Container process %d is still alive but not a direct child", container_pid);
|
||||
return;
|
||||
} else if (errno == ESRCH) {
|
||||
/* Container process has exited */
|
||||
ninfof("Container process %d has exited (detected via kill probe)", container_pid);
|
||||
/* Simulate container exit callback */
|
||||
container_status = 0; /* We can't get the real exit status */
|
||||
container_pid = -1;
|
||||
/* Fall through to quit the main loop */
|
||||
}
|
||||
}
|
||||
g_main_loop_quit(main_loop);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#include "ctr_logging.h"
|
||||
#include "cli.h"
|
||||
#include "config.h"
|
||||
#include <ctype.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
|
|
@ -70,6 +71,7 @@ static char *container_id_full = NULL;
|
|||
static char *container_id = NULL;
|
||||
static char *container_name = NULL;
|
||||
static char *container_tag = NULL;
|
||||
static gchar **container_labels = NULL;
|
||||
static size_t container_tag_len;
|
||||
static char *syslog_identifier = NULL;
|
||||
static size_t syslog_identifier_len;
|
||||
|
|
@ -87,7 +89,7 @@ static bool get_line_len(ptrdiff_t *line_len, const char *buf, ssize_t buflen);
|
|||
static ssize_t writev_buffer_append_segment(int fd, writev_buffer_t *buf, const void *data, ssize_t len);
|
||||
static ssize_t writev_buffer_append_segment_no_flush(writev_buffer_t *buf, const void *data, ssize_t len);
|
||||
static ssize_t writev_buffer_flush(int fd, writev_buffer_t *buf);
|
||||
static int set_k8s_timestamp(char *buf, ssize_t buflen, const char *pipename);
|
||||
static void set_k8s_timestamp(char *buf, ssize_t buflen, const char *pipename);
|
||||
static void reopen_k8s_file(void);
|
||||
|
||||
|
||||
|
|
@ -96,13 +98,43 @@ gboolean logging_is_passthrough(void)
|
|||
return use_logging_passthrough;
|
||||
}
|
||||
|
||||
static int count_chars_in_string(const char *str, char ch)
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
while (str) {
|
||||
str = strchr(str, ch);
|
||||
if (str == NULL)
|
||||
break;
|
||||
count++;
|
||||
str++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
static int is_valid_label_name(const char *str)
|
||||
{
|
||||
while (*str) {
|
||||
if (*str == '=') {
|
||||
return 1;
|
||||
}
|
||||
if (!isupper(*str) && !isdigit(*str) && *str != '_') {
|
||||
return 0;
|
||||
}
|
||||
str++;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* configures container log specific information, such as the drivers the user
|
||||
* called with and the max log size for log file types. For the log file types
|
||||
* (currently just k8s log file), it will also open the log_fd for that specific
|
||||
* log file.
|
||||
*/
|
||||
void configure_log_drivers(gchar **log_drivers, int64_t log_size_max_, int64_t log_global_size_max_, char *cuuid_, char *name_, char *tag)
|
||||
void configure_log_drivers(gchar **log_drivers, int64_t log_size_max_, int64_t log_global_size_max_, char *cuuid_, char *name_, char *tag,
|
||||
gchar **log_labels)
|
||||
{
|
||||
log_size_max = log_size_max_;
|
||||
log_global_size_max = log_global_size_max_;
|
||||
|
|
@ -113,7 +145,7 @@ void configure_log_drivers(gchar **log_drivers, int64_t log_size_max_, int64_t l
|
|||
}
|
||||
if (use_k8s_logging) {
|
||||
/* Open the log path file. */
|
||||
k8s_log_fd = open(k8s_log_path, O_WRONLY | O_APPEND | O_CREAT | O_CLOEXEC, 0600);
|
||||
k8s_log_fd = open(k8s_log_path, O_WRONLY | O_APPEND | O_CREAT | O_CLOEXEC, 0640);
|
||||
if (k8s_log_fd < 0)
|
||||
pexit("Failed to open log file");
|
||||
|
||||
|
|
@ -126,8 +158,14 @@ void configure_log_drivers(gchar **log_drivers, int64_t log_size_max_, int64_t l
|
|||
}
|
||||
k8s_total_bytes_written = k8s_bytes_written;
|
||||
|
||||
if (!use_journald_logging && tag)
|
||||
nexit("k8s-file doesn't support --log-tag");
|
||||
if (!use_journald_logging) {
|
||||
if (tag) {
|
||||
nexit("k8s-file doesn't support --log-tag");
|
||||
}
|
||||
if (log_labels) {
|
||||
nexit("k8s-file doesn't support --log-label");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (use_journald_logging) {
|
||||
|
|
@ -169,6 +207,24 @@ void configure_log_drivers(gchar **log_drivers, int64_t log_size_max_, int64_t l
|
|||
syslog_identifier = g_strdup_printf("SYSLOG_IDENTIFIER=%s", tag);
|
||||
syslog_identifier_len = strlen(syslog_identifier);
|
||||
}
|
||||
if (log_labels) {
|
||||
container_labels = log_labels;
|
||||
|
||||
/* Ensure that valid LABEL=VALUE pairs have been passed */
|
||||
for (char **ptr = log_labels; *ptr; ptr++) {
|
||||
if (**ptr == '=') {
|
||||
nexitf("Container labels must be in format LABEL=VALUE (no LABEL present in '%s')", *ptr);
|
||||
}
|
||||
if (count_chars_in_string(*ptr, '=') != 1) {
|
||||
nexitf("Container labels must be in format LABEL=VALUE (none or more than one '=' present in '%s')",
|
||||
*ptr);
|
||||
}
|
||||
if (!is_valid_label_name(*ptr)) {
|
||||
nexitf("Container label names must contain only uppercase letters, numbers and underscore (in '%s')",
|
||||
*ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -325,10 +381,16 @@ static int write_journald(int pipe, char *buf, ssize_t buflen)
|
|||
/* per docker journald logging format, CONTAINER_PARTIAL_MESSAGE is set to true if it's partial, but otherwise not set. */
|
||||
if (partial && writev_buffer_append_segment_no_flush(&bufv, "CONTAINER_PARTIAL_MESSAGE=true", PARTIAL_MESSAGE_EQ_LEN) < 0)
|
||||
return -1;
|
||||
if (container_labels) {
|
||||
for (gchar **label = container_labels; *label; ++label) {
|
||||
if (writev_buffer_append_segment_no_flush(&bufv, *label, strlen(*label)) < 0)
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int err = sd_journal_sendv(bufv.iov, bufv.iovcnt);
|
||||
if (err < 0) {
|
||||
pwarn(strerror(-err));
|
||||
nwarnf("sd_journal_sendv: %s", strerror(-err));
|
||||
return err;
|
||||
}
|
||||
|
||||
|
|
@ -356,9 +418,7 @@ static int write_k8s_log(stdpipe_t pipe, const char *buf, ssize_t buflen)
|
|||
* fast.
|
||||
*/
|
||||
char tsbuf[TSBUFLEN];
|
||||
if (set_k8s_timestamp(tsbuf, sizeof tsbuf, stdpipe_name(pipe)))
|
||||
/* TODO: We should handle failures much more cleanly than this. */
|
||||
return -1;
|
||||
set_k8s_timestamp(tsbuf, sizeof tsbuf, stdpipe_name(pipe));
|
||||
|
||||
ptrdiff_t line_len = 0;
|
||||
while (buflen > 0) {
|
||||
|
|
@ -384,12 +444,13 @@ static int write_k8s_log(stdpipe_t pipe, const char *buf, ssize_t buflen)
|
|||
if ((log_size_max > 0) && (k8s_bytes_written + bytes_to_be_written) > log_size_max) {
|
||||
if (writev_buffer_flush(k8s_log_fd, &bufv) < 0) {
|
||||
nwarn("failed to flush buffer to log");
|
||||
/*
|
||||
* We are going to reopen the file anyway, in case of
|
||||
* errors discard all we have in the buffer.
|
||||
*/
|
||||
bufv.iovcnt = 0;
|
||||
}
|
||||
/*
|
||||
* Always reset the buffer after rotation to ensure clean state
|
||||
* with the new file descriptor. Any unflushed data is lost, but
|
||||
* this prevents corruption of subsequent log entries.
|
||||
*/
|
||||
bufv.iovcnt = 0;
|
||||
reopen_k8s_file();
|
||||
}
|
||||
|
||||
|
|
@ -460,35 +521,28 @@ static bool get_line_len(ptrdiff_t *line_len, const char *buf, ssize_t buflen)
|
|||
static ssize_t writev_buffer_flush(int fd, writev_buffer_t *buf)
|
||||
{
|
||||
size_t count = 0;
|
||||
int iovcnt = buf->iovcnt;
|
||||
struct iovec *iov = buf->iov;
|
||||
|
||||
while (iovcnt > 0) {
|
||||
ssize_t res;
|
||||
do {
|
||||
res = writev(fd, iov, iovcnt);
|
||||
} while (res == -1 && errno == EINTR);
|
||||
for (int i = 0; i < buf->iovcnt; i++) {
|
||||
const char *ptr = buf->iov[i].iov_base;
|
||||
size_t remaining = buf->iov[i].iov_len;
|
||||
|
||||
if (res <= 0)
|
||||
return -1;
|
||||
|
||||
count += res;
|
||||
|
||||
while (res > 0) {
|
||||
size_t from_this = MIN((size_t)res, iov->iov_len);
|
||||
iov->iov_len -= from_this;
|
||||
iov->iov_base += from_this;
|
||||
res -= from_this;
|
||||
|
||||
if (iov->iov_len == 0) {
|
||||
iov++;
|
||||
iovcnt--;
|
||||
while (remaining > 0) {
|
||||
ssize_t written = write(fd, ptr, remaining);
|
||||
if (written < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return -1;
|
||||
}
|
||||
if (written == 0)
|
||||
return -1;
|
||||
|
||||
ptr += written;
|
||||
remaining -= written;
|
||||
count += written;
|
||||
}
|
||||
}
|
||||
|
||||
buf->iovcnt = 0;
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
|
|
@ -542,46 +596,57 @@ static const char *stdpipe_name(stdpipe_t pipe)
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
static int set_k8s_timestamp(char *buf, ssize_t buflen, const char *pipename)
|
||||
/* Generate timestamp string to buf. */
|
||||
static void set_k8s_timestamp(char *buf, ssize_t buflen, const char *pipename)
|
||||
{
|
||||
static int tzset_called = 0;
|
||||
int err = -1;
|
||||
|
||||
struct timespec ts;
|
||||
/* Initialize timestamp variables with sensible defaults. */
|
||||
struct timespec ts = {0};
|
||||
struct tm current_tm = {0};
|
||||
char off_sign = '+';
|
||||
int off = 0;
|
||||
|
||||
/* Attempt to get the current time. */
|
||||
if (clock_gettime(CLOCK_REALTIME, &ts) < 0) {
|
||||
/* If CLOCK_REALTIME is not supported, we set nano seconds to 0 */
|
||||
if (errno == EINVAL) {
|
||||
ts.tv_nsec = 0;
|
||||
} else {
|
||||
return err;
|
||||
if (errno != EINVAL) {
|
||||
ts.tv_nsec = 0; /* If other errors, fallback to nanoseconds = 0. */
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure tzset is called only once. */
|
||||
if (!tzset_called) {
|
||||
tzset();
|
||||
tzset_called = 1;
|
||||
}
|
||||
|
||||
struct tm current_tm;
|
||||
if (localtime_r(&ts.tv_sec, ¤t_tm) == NULL)
|
||||
return err;
|
||||
/* Get the local time or fallback to defaults. */
|
||||
if (localtime_r(&ts.tv_sec, ¤t_tm) == NULL) {
|
||||
current_tm.tm_year = 70; /* 1970 (default epoch year) */
|
||||
current_tm.tm_mon = 0; /* January */
|
||||
current_tm.tm_mday = 1; /* 1st day of the month */
|
||||
current_tm.tm_hour = 0; /* midnight */
|
||||
current_tm.tm_min = 0;
|
||||
current_tm.tm_sec = 0;
|
||||
current_tm.tm_gmtoff = 0; /* UTC offset */
|
||||
}
|
||||
|
||||
|
||||
char off_sign = '+';
|
||||
int off = (int)current_tm.tm_gmtoff;
|
||||
if (current_tm.tm_gmtoff < 0) {
|
||||
/* Calculate timezone offset. */
|
||||
off = (int)current_tm.tm_gmtoff;
|
||||
if (off < 0) {
|
||||
off_sign = '-';
|
||||
off = -off;
|
||||
}
|
||||
|
||||
/* Format the timestamp into the buffer. */
|
||||
int len = snprintf(buf, buflen, "%d-%02d-%02dT%02d:%02d:%02d.%09ld%c%02d:%02d %s ", current_tm.tm_year + 1900,
|
||||
current_tm.tm_mon + 1, current_tm.tm_mday, current_tm.tm_hour, current_tm.tm_min, current_tm.tm_sec, ts.tv_nsec,
|
||||
off_sign, off / 3600, (off % 3600) / 60, pipename);
|
||||
|
||||
if (len < buflen)
|
||||
err = 0;
|
||||
return err;
|
||||
/* Ensure null termination if snprintf output exceeds buffer length. */
|
||||
if (len >= buflen && buflen > 0) {
|
||||
buf[buflen - 1] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
/* Force closing any open FD. */
|
||||
|
|
@ -606,11 +671,6 @@ static void reopen_k8s_file(void)
|
|||
|
||||
_cleanup_free_ char *k8s_log_path_tmp = g_strdup_printf("%s.tmp", k8s_log_path);
|
||||
|
||||
/* Sync the logs to disk */
|
||||
if (!opt_no_sync_log && fsync(k8s_log_fd) < 0) {
|
||||
pwarn("Failed to sync log file on reopen");
|
||||
}
|
||||
|
||||
/* Close the current k8s_log_fd */
|
||||
close(k8s_log_fd);
|
||||
|
||||
|
|
@ -618,7 +678,7 @@ static void reopen_k8s_file(void)
|
|||
k8s_bytes_written = 0;
|
||||
|
||||
/* Open the log path file again */
|
||||
k8s_log_fd = open(k8s_log_path_tmp, O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, 0600);
|
||||
k8s_log_fd = open(k8s_log_path_tmp, O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, 0640);
|
||||
if (k8s_log_fd < 0)
|
||||
pexitf("Failed to open log file %s", k8s_log_path);
|
||||
|
||||
|
|
@ -634,5 +694,5 @@ void sync_logs(void)
|
|||
/* Sync the logs to disk */
|
||||
if (k8s_log_fd > 0)
|
||||
if (fsync(k8s_log_fd) < 0)
|
||||
pwarn("Failed to sync log file before exit");
|
||||
nwarnf("Failed to sync log file before exit: %m");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@
|
|||
|
||||
void reopen_log_files(void);
|
||||
bool write_to_logs(stdpipe_t pipe, char *buf, ssize_t num_read);
|
||||
void configure_log_drivers(gchar **log_drivers, int64_t log_size_max_, int64_t log_global_size_max_, char *cuuid_, char *name_, char *tag);
|
||||
void configure_log_drivers(gchar **log_drivers, int64_t log_size_max_, int64_t log_global_size_max_, char *cuuid_, char *name_, char *tag,
|
||||
gchar **labels);
|
||||
void sync_logs(void);
|
||||
gboolean logging_is_passthrough(void);
|
||||
void close_logging_fds(void);
|
||||
|
|
|
|||
|
|
@ -129,6 +129,10 @@ static bool read_stdio(int fd, stdpipe_t pipe, gboolean *eof)
|
|||
*eof = true;
|
||||
return false;
|
||||
} else if (num_read < 0) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
// Non-blocking mode - no data available, return gracefully
|
||||
return true;
|
||||
}
|
||||
/* Ignore EIO if fd is a tty, since this can happen when the tty is closed
|
||||
while we are reading from it. */
|
||||
if (errno == EIO && isatty(fd)) {
|
||||
|
|
@ -136,7 +140,7 @@ static bool read_stdio(int fd, stdpipe_t pipe, gboolean *eof)
|
|||
*eof = true;
|
||||
return false;
|
||||
}
|
||||
nwarnf("stdio_input read failed %s", strerror(errno));
|
||||
nwarnf("stdio_input read failed: %m");
|
||||
return false;
|
||||
} else {
|
||||
// Always null terminate the buffer, just in case.
|
||||
|
|
@ -144,7 +148,7 @@ static bool read_stdio(int fd, stdpipe_t pipe, gboolean *eof)
|
|||
|
||||
bool written = write_to_logs(pipe, buf, num_read);
|
||||
if (!written)
|
||||
return written;
|
||||
return false;
|
||||
|
||||
real_buf[0] = pipe;
|
||||
write_back_to_remote_consoles(real_buf, num_read + 1);
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ static gboolean read_from_ctrl_buffer(int fd, gboolean (*line_process_func)(char
|
|||
char *newline = strchrnul(beg, '\n');
|
||||
/* Process each message which ends with a line */
|
||||
while (*newline != '\0') {
|
||||
if (!line_process_func(ctlbuf))
|
||||
if (!line_process_func(beg))
|
||||
return G_SOURCE_CONTINUE;
|
||||
|
||||
beg = newline + 1;
|
||||
|
|
@ -231,7 +231,7 @@ static void resize_winsz(int height, int width)
|
|||
|
||||
int ret = ioctl(mainfd_stdout, TIOCSWINSZ, &ws);
|
||||
if (ret == -1)
|
||||
pwarn("Failed to set process pty terminal size");
|
||||
nwarnf("Failed to set process pty terminal size: %m");
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -15,18 +15,18 @@ static void write_oom_adjust(int oom_score, int *old_value)
|
|||
char fmt_oom_score[16];
|
||||
int oom_score_fd = open("/proc/self/oom_score_adj", O_RDWR | O_CLOEXEC);
|
||||
if (oom_score_fd < 0) {
|
||||
ndebugf("failed to open /proc/self/oom_score_adj: %s\n", strerror(errno));
|
||||
ndebugf("failed to open /proc/self/oom_score_adj: %m");
|
||||
return;
|
||||
}
|
||||
if (old_value) {
|
||||
if (read(oom_score_fd, fmt_oom_score, sizeof(fmt_oom_score)) < 0) {
|
||||
ndebugf("failed to read from /proc/self/oom_score_adj: %s\n", strerror(errno));
|
||||
ndebugf("failed to read from /proc/self/oom_score_adj: %m");
|
||||
}
|
||||
*old_value = atoi(fmt_oom_score);
|
||||
}
|
||||
sprintf(fmt_oom_score, "%d", oom_score);
|
||||
snprintf(fmt_oom_score, sizeof(fmt_oom_score), "%d", oom_score);
|
||||
if (write(oom_score_fd, fmt_oom_score, strlen(fmt_oom_score)) < 0) {
|
||||
ndebugf("failed to write to /proc/self/oom_score_adj: %s\n", strerror(errno));
|
||||
ndebugf("failed to write to /proc/self/oom_score_adj: %m");
|
||||
}
|
||||
close(oom_score_fd);
|
||||
#else
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ gboolean seccomp_accept_cb(int fd, G_GNUC_UNUSED GIOCondition condition, G_GNUC_
|
|||
if (listener.fd < 0) {
|
||||
pexit("Failed to receive socket listener file descriptor");
|
||||
}
|
||||
free(listener.name);
|
||||
|
||||
_cleanup_free_ char *oci_config_path = g_strdup_printf("%s/config.json", opt_bundle_path);
|
||||
if (oci_config_path == NULL) {
|
||||
|
|
|
|||
|
|
@ -49,17 +49,10 @@ void set_conmon_logs(char *level_name, char *cid_, gboolean syslog_, char *tag)
|
|||
nexitf("No such log level %s", level_name);
|
||||
}
|
||||
|
||||
#ifdef __FreeBSD__
|
||||
static bool retryable_error(int err)
|
||||
{
|
||||
return err == EINTR || err == EAGAIN;
|
||||
return err == EINTR || err == EAGAIN || err == ENOBUFS;
|
||||
}
|
||||
#else
|
||||
static bool retryable_error(int err)
|
||||
{
|
||||
return err == EINTR;
|
||||
}
|
||||
#endif
|
||||
|
||||
static void get_signal_descriptor_mask(sigset_t *set)
|
||||
{
|
||||
|
|
|
|||
87
src/utils.h
87
src/utils.h
|
|
@ -6,6 +6,7 @@
|
|||
#include <syslog.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <glib.h>
|
||||
#include <glib-unix.h>
|
||||
#include <sys/uio.h>
|
||||
|
|
@ -35,48 +36,58 @@ extern log_level_t log_level;
|
|||
extern char *log_cid;
|
||||
extern gboolean use_syslog;
|
||||
|
||||
|
||||
#define _pexit(s) \
|
||||
do { \
|
||||
fprintf(stderr, "[conmon:e]: %s %s\n", s, strerror(errno)); \
|
||||
if (use_syslog) \
|
||||
syslog(LOG_ERR, "conmon %.20s <error>: %s %s\n", log_cid, s, strerror(errno)); \
|
||||
int saved_errno = errno; \
|
||||
errno = saved_errno; \
|
||||
fprintf(stderr, "[conmon:e]: %s %m\n", s); \
|
||||
if (use_syslog) { \
|
||||
errno = saved_errno; \
|
||||
syslog(LOG_ERR, "conmon %.20s <error>: %s %m\n", log_cid, s); \
|
||||
} \
|
||||
_exit(EXIT_FAILURE); \
|
||||
} while (0)
|
||||
|
||||
#define _pexitf(fmt, ...) \
|
||||
do { \
|
||||
fprintf(stderr, "[conmon:e]: " fmt " %s\n", ##__VA_ARGS__, strerror(errno)); \
|
||||
if (use_syslog) \
|
||||
syslog(LOG_ERR, "conmon %.20s <error>: " fmt ": %s\n", log_cid, ##__VA_ARGS__, strerror(errno)); \
|
||||
int saved_errno = errno; \
|
||||
errno = saved_errno; \
|
||||
fprintf(stderr, "[conmon:e]: " fmt " %m\n", ##__VA_ARGS__); \
|
||||
if (use_syslog) { \
|
||||
errno = saved_errno; \
|
||||
syslog(LOG_ERR, "conmon %.20s <error>: " fmt ": %m\n", log_cid, ##__VA_ARGS__); \
|
||||
} \
|
||||
_exit(EXIT_FAILURE); \
|
||||
} while (0)
|
||||
|
||||
#define pexit(s) \
|
||||
do { \
|
||||
fprintf(stderr, "[conmon:e]: %s %s\n", s, strerror(errno)); \
|
||||
if (use_syslog) \
|
||||
syslog(LOG_ERR, "conmon %.20s <error>: %s %s\n", log_cid, s, strerror(errno)); \
|
||||
int saved_errno = errno; \
|
||||
errno = saved_errno; \
|
||||
fprintf(stderr, "[conmon:e]: %s %m\n", s); \
|
||||
if (use_syslog) { \
|
||||
errno = saved_errno; \
|
||||
syslog(LOG_ERR, "conmon %.20s <error>: %s %m\n", log_cid, s); \
|
||||
} \
|
||||
exit(EXIT_FAILURE); \
|
||||
} while (0)
|
||||
|
||||
#define pexitf(fmt, ...) \
|
||||
do { \
|
||||
fprintf(stderr, "[conmon:e]: " fmt " %s\n", ##__VA_ARGS__, strerror(errno)); \
|
||||
if (use_syslog) \
|
||||
syslog(LOG_ERR, "conmon %.20s <error>: " fmt ": %s\n", log_cid, ##__VA_ARGS__, strerror(errno)); \
|
||||
int saved_errno = errno; \
|
||||
errno = saved_errno; \
|
||||
fprintf(stderr, "[conmon:e]: " fmt " %m\n", ##__VA_ARGS__); \
|
||||
if (use_syslog) { \
|
||||
errno = saved_errno; \
|
||||
syslog(LOG_ERR, "conmon %.20s <error>: " fmt ": %m\n", log_cid, ##__VA_ARGS__); \
|
||||
} \
|
||||
exit(EXIT_FAILURE); \
|
||||
} while (0)
|
||||
|
||||
#define pwarn(s) \
|
||||
do { \
|
||||
fprintf(stderr, "[conmon:w]: %s %s\n", s, strerror(errno)); \
|
||||
if (use_syslog) \
|
||||
syslog(LOG_INFO, "conmon %.20s <pwarn>: %s %s\n", log_cid, s, strerror(errno)); \
|
||||
} while (0)
|
||||
|
||||
#define nexit(s) \
|
||||
do { \
|
||||
fprintf(stderr, "[conmon:e] %s\n", s); \
|
||||
fprintf(stderr, "[conmon:e]: %s\n", s); \
|
||||
if (use_syslog) \
|
||||
syslog(LOG_ERR, "conmon %.20s <error>: %s\n", log_cid, s); \
|
||||
exit(EXIT_FAILURE); \
|
||||
|
|
@ -86,10 +97,36 @@ extern gboolean use_syslog;
|
|||
do { \
|
||||
fprintf(stderr, "[conmon:e]: " fmt "\n", ##__VA_ARGS__); \
|
||||
if (use_syslog) \
|
||||
syslog(LOG_ERR, "conmon %.20s <error>: " fmt " \n", log_cid, ##__VA_ARGS__); \
|
||||
syslog(LOG_ERR, "conmon %.20s <error>: " fmt "\n", log_cid, ##__VA_ARGS__); \
|
||||
exit(EXIT_FAILURE); \
|
||||
} while (0)
|
||||
|
||||
#define pwarn(s) \
|
||||
do { \
|
||||
if (log_level >= WARN_LEVEL) { \
|
||||
int saved_errno = errno; \
|
||||
errno = saved_errno; \
|
||||
fprintf(stderr, "[conmon:w]: %s %m\n", s); \
|
||||
if (use_syslog) { \
|
||||
errno = saved_errno; \
|
||||
syslog(LOG_INFO, "conmon %.20s <pwarn>: %s %m\n", log_cid, s); \
|
||||
} \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define pwarnf(fmt, ...) \
|
||||
do { \
|
||||
if (log_level >= WARN_LEVEL) { \
|
||||
int saved_errno = errno; \
|
||||
errno = saved_errno; \
|
||||
fprintf(stderr, "[conmon:w]: " fmt " %m\n", ##__VA_ARGS__); \
|
||||
if (use_syslog) { \
|
||||
errno = saved_errno; \
|
||||
syslog(LOG_INFO, "conmon %.20s <pwarnf>: " fmt ": %m\n", log_cid, ##__VA_ARGS__); \
|
||||
} \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define nwarn(s) \
|
||||
if (log_level >= WARN_LEVEL) { \
|
||||
do { \
|
||||
|
|
@ -104,7 +141,7 @@ extern gboolean use_syslog;
|
|||
do { \
|
||||
fprintf(stderr, "[conmon:w]: " fmt "\n", ##__VA_ARGS__); \
|
||||
if (use_syslog) \
|
||||
syslog(LOG_INFO, "conmon %.20s <nwarn>: " fmt " \n", log_cid, ##__VA_ARGS__); \
|
||||
syslog(LOG_INFO, "conmon %.20s <nwarn>: " fmt "\n", log_cid, ##__VA_ARGS__); \
|
||||
} while (0); \
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +159,7 @@ extern gboolean use_syslog;
|
|||
do { \
|
||||
fprintf(stderr, "[conmon:i]: " fmt "\n", ##__VA_ARGS__); \
|
||||
if (use_syslog) \
|
||||
syslog(LOG_INFO, "conmon %.20s <ninfo>: " fmt " \n", log_cid, ##__VA_ARGS__); \
|
||||
syslog(LOG_INFO, "conmon %.20s <ninfo>: " fmt "\n", log_cid, ##__VA_ARGS__); \
|
||||
} while (0); \
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +177,7 @@ extern gboolean use_syslog;
|
|||
do { \
|
||||
fprintf(stderr, "[conmon:d]: " fmt "\n", ##__VA_ARGS__); \
|
||||
if (use_syslog) \
|
||||
syslog(LOG_INFO, "conmon %.20s <ndebug>: " fmt " \n", log_cid, ##__VA_ARGS__); \
|
||||
syslog(LOG_INFO, "conmon %.20s <ndebug>: " fmt "\n", log_cid, ##__VA_ARGS__); \
|
||||
} while (0); \
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +195,7 @@ extern gboolean use_syslog;
|
|||
do { \
|
||||
fprintf(stderr, "[conmon:d]: " fmt "\n", ##__VA_ARGS__); \
|
||||
if (use_syslog) \
|
||||
syslog(LOG_INFO, "conmon %.20s <ntrace>: " fmt " \n", log_cid, ##__VA_ARGS__); \
|
||||
syslog(LOG_INFO, "conmon %.20s <ntrace>: " fmt "\n", log_cid, ##__VA_ARGS__); \
|
||||
} while (0); \
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,174 @@
|
|||
#!/usr/bin/env bats
|
||||
|
||||
load test_helper
|
||||
|
||||
setup() {
|
||||
check_conmon_binary
|
||||
setup_test_env
|
||||
}
|
||||
|
||||
teardown() {
|
||||
cleanup_test_env
|
||||
}
|
||||
|
||||
@test "conmon version" {
|
||||
run_conmon --version
|
||||
assert_success
|
||||
assert_output_contains "conmon version"
|
||||
assert_output_contains "commit"
|
||||
}
|
||||
|
||||
@test "no container ID should fail" {
|
||||
run_conmon
|
||||
assert_failure
|
||||
assert_output_contains "Container ID not provided. Use --cid"
|
||||
}
|
||||
|
||||
@test "no container UUID should fail" {
|
||||
run_conmon --cid "$CTR_ID"
|
||||
assert_failure
|
||||
assert_output_contains "Container UUID not provided. Use --cuuid"
|
||||
}
|
||||
|
||||
@test "no runtime path should fail" {
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID"
|
||||
assert_failure
|
||||
assert_output_contains "Runtime path not provided. Use --runtime"
|
||||
}
|
||||
|
||||
@test "invalid runtime path should fail" {
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$INVALID_PATH"
|
||||
assert_failure
|
||||
assert_output_contains "Runtime path $INVALID_PATH is not valid"
|
||||
}
|
||||
|
||||
@test "no log driver should fail" {
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH"
|
||||
assert_failure
|
||||
assert_output_contains "Log driver not provided. Use --log-path"
|
||||
}
|
||||
|
||||
@test "empty log driver should fail" {
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH" --log-path ""
|
||||
assert_failure
|
||||
assert_output_contains "log-path must not be empty"
|
||||
}
|
||||
|
||||
@test "empty log driver and path should fail" {
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH" --log-path ":"
|
||||
assert_failure
|
||||
assert_output_contains "log-path must not be empty"
|
||||
}
|
||||
|
||||
@test "k8s-file requires a filename" {
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH" --log-path "k8s-file"
|
||||
assert_failure
|
||||
assert_output_contains "k8s-file requires a filename"
|
||||
}
|
||||
|
||||
@test "k8s-file: requires a filename" {
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH" --log-path "k8s-file:"
|
||||
assert_failure
|
||||
assert_output_contains "k8s-file requires a filename"
|
||||
}
|
||||
|
||||
@test "log driver as path should pass" {
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH" --log-path "$LOG_PATH"
|
||||
assert_success
|
||||
[ -f "$LOG_PATH" ]
|
||||
}
|
||||
|
||||
@test "log driver as k8s-file:path should pass" {
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH" --log-path "k8s-file:$LOG_PATH"
|
||||
assert_success
|
||||
[ -f "$LOG_PATH" ]
|
||||
}
|
||||
|
||||
@test "log driver as :path should pass" {
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH" --log-path ":$LOG_PATH"
|
||||
assert_success
|
||||
[ -f "$LOG_PATH" ]
|
||||
}
|
||||
|
||||
@test "log driver as none should pass" {
|
||||
cd "$TEST_TMPDIR"
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH" --log-path "none:"
|
||||
assert_success
|
||||
[ ! -f "none" ]
|
||||
}
|
||||
|
||||
@test "log driver as off should pass" {
|
||||
cd "$TEST_TMPDIR"
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH" --log-path "off:"
|
||||
assert_success
|
||||
[ ! -f "off" ]
|
||||
}
|
||||
|
||||
@test "log driver as null should pass" {
|
||||
cd "$TEST_TMPDIR"
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH" --log-path "null:"
|
||||
assert_success
|
||||
[ ! -f "null" ]
|
||||
}
|
||||
|
||||
@test "log driver as journald should pass" {
|
||||
cd "$TEST_TMPDIR"
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH" --log-path "journald:"
|
||||
assert_success
|
||||
[ ! -f "journald" ]
|
||||
}
|
||||
|
||||
@test "log driver as :journald should pass" {
|
||||
cd "$TEST_TMPDIR"
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH" --log-path ":journald"
|
||||
assert_success
|
||||
[ -f "journald" ]
|
||||
}
|
||||
|
||||
@test "log driver as journald with short cid should fail" {
|
||||
local short_ctr_id="abcdefghijkl"
|
||||
run_conmon --cid "$short_ctr_id" --cuuid "$short_ctr_id" --runtime "$VALID_PATH" --log-path "journald:"
|
||||
assert_failure
|
||||
assert_output_contains "Container ID must be longer than 12 characters"
|
||||
}
|
||||
|
||||
@test "log driver as k8s-file with path should pass" {
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH" --log-path "k8s-file:$LOG_PATH"
|
||||
assert_success
|
||||
[ -f "$LOG_PATH" ]
|
||||
}
|
||||
|
||||
@test "log driver as k8s-file with invalid path should fail" {
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH" --log-path "k8s-file:$INVALID_PATH"
|
||||
assert_failure
|
||||
assert_output_contains "Failed to open log file"
|
||||
}
|
||||
|
||||
@test "log driver as invalid driver should fail" {
|
||||
local invalid_log_driver="invalid"
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH" --log-path "$invalid_log_driver:$LOG_PATH"
|
||||
assert_failure
|
||||
assert_output_contains "No such log driver $invalid_log_driver"
|
||||
}
|
||||
|
||||
@test "log driver as invalid driver with blank path should fail" {
|
||||
local invalid_log_driver="invalid"
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH" --log-path "$invalid_log_driver:"
|
||||
assert_failure
|
||||
assert_output_contains "No such log driver $invalid_log_driver"
|
||||
}
|
||||
|
||||
@test "multiple log drivers should pass" {
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH" \
|
||||
--log-path "k8s-file:$LOG_PATH" --log-path "journald:"
|
||||
assert_success
|
||||
[ -f "$LOG_PATH" ]
|
||||
}
|
||||
|
||||
@test "multiple log drivers with one invalid should fail" {
|
||||
local invalid_log_driver="invalid"
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH" \
|
||||
--log-path "k8s-file:$LOG_PATH" --log-path "$invalid_log_driver:$LOG_PATH"
|
||||
assert_failure
|
||||
assert_output_contains "No such log driver $invalid_log_driver"
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
#!/usr/bin/env bats
|
||||
|
||||
load test_helper
|
||||
|
||||
setup() {
|
||||
check_conmon_binary
|
||||
setup_test_env
|
||||
}
|
||||
|
||||
teardown() {
|
||||
cleanup_test_env
|
||||
}
|
||||
|
||||
# Helper function to run conmon with basic log options
|
||||
run_conmon_with_log_opts() {
|
||||
local extra_args=("$@")
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH" "${extra_args[@]}"
|
||||
}
|
||||
|
||||
@test "ctr logs: no log driver should fail" {
|
||||
run_conmon_with_log_opts
|
||||
assert_failure
|
||||
assert_output_contains "Log driver not provided. Use --log-path"
|
||||
}
|
||||
|
||||
@test "ctr logs: log driver as path should pass" {
|
||||
run_conmon_with_log_opts --log-path "$LOG_PATH"
|
||||
assert_success
|
||||
[ -f "$LOG_PATH" ]
|
||||
}
|
||||
|
||||
@test "ctr logs: log driver as journald should pass" {
|
||||
run_conmon_with_log_opts --log-path "journald:"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "ctr logs: log driver as passthrough should pass" {
|
||||
run_conmon_with_log_opts --log-path "passthrough:"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "ctr logs: log driver as k8s-file with invalid path should fail" {
|
||||
run_conmon_with_log_opts --log-path "k8s-file:$INVALID_PATH"
|
||||
assert_failure
|
||||
assert_output_contains "Failed to open log file"
|
||||
}
|
||||
|
||||
@test "ctr logs: log driver as invalid driver should fail" {
|
||||
local invalid_log_driver="invalid"
|
||||
run_conmon_with_log_opts --log-path "$invalid_log_driver:$LOG_PATH"
|
||||
assert_failure
|
||||
assert_output_contains "No such log driver $invalid_log_driver"
|
||||
}
|
||||
|
||||
@test "ctr logs: multiple log drivers should pass" {
|
||||
run_conmon_with_log_opts --log-path "k8s-file:$LOG_PATH" --log-path "journald:"
|
||||
assert_success
|
||||
[ -f "$LOG_PATH" ]
|
||||
}
|
||||
|
||||
@test "ctr logs: multiple log drivers with one invalid should fail" {
|
||||
local invalid_log_driver="invalid"
|
||||
run_conmon_with_log_opts --log-path "k8s-file:$LOG_PATH" --log-path "$invalid_log_driver:$LOG_PATH"
|
||||
assert_failure
|
||||
assert_output_contains "No such log driver $invalid_log_driver"
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
#!/usr/bin/env bats
|
||||
|
||||
# k8s_log_rotation_test.bats
|
||||
#
|
||||
# This test suite validates the k8s-file log rotation fix implemented in commit 29d17be.
|
||||
# The fix addressed log corruption during log rotation where writev_buffer_flush() was
|
||||
# incorrectly handling partial writes, causing corrupted buffer state to carry over to
|
||||
# new file descriptors after rotation.
|
||||
#
|
||||
# The tests focus on:
|
||||
# 1. Basic k8s-file log driver functionality with log-size-max option
|
||||
# 2. Validation that small log size limits are accepted without errors
|
||||
# 3. Edge case testing with very small rotation thresholds
|
||||
# 4. Log file creation and content integrity validation
|
||||
#
|
||||
# While these tests don't create actual running containers (to avoid test environment
|
||||
# dependencies), they validate that the conmon command-line options work correctly and
|
||||
# that log files can be created and managed properly. The real fix prevents buffer
|
||||
# corruption during writev operations when log rotation occurs, which would have
|
||||
# manifested as malformed k8s log entries with repeated timestamps and broken formatting.
|
||||
|
||||
load test_helper
|
||||
|
||||
setup() {
|
||||
check_conmon_binary
|
||||
setup_test_env
|
||||
}
|
||||
|
||||
teardown() {
|
||||
cleanup_test_env
|
||||
}
|
||||
|
||||
# Helper function to run conmon with k8s-file log driver
|
||||
run_conmon_k8s_file() {
|
||||
local extra_args=("$@")
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH" \
|
||||
--log-path "k8s-file:$LOG_PATH" "${extra_args[@]}"
|
||||
}
|
||||
|
||||
@test "k8s log rotation: should create valid k8s log format" {
|
||||
run_conmon_k8s_file
|
||||
assert_success
|
||||
[ -f "$LOG_PATH" ]
|
||||
}
|
||||
|
||||
@test "k8s log rotation: should accept log-size-max option" {
|
||||
local log_size_max=1024
|
||||
run_conmon_k8s_file --log-size-max "$log_size_max"
|
||||
assert_success
|
||||
[ -f "$LOG_PATH" ]
|
||||
}
|
||||
|
||||
@test "k8s log rotation: should handle multiple log drivers with size limits" {
|
||||
local log_size_max=2048
|
||||
run_conmon --cid "$CTR_ID" --cuuid "$CTR_ID" --runtime "$VALID_PATH" \
|
||||
--log-path "k8s-file:$LOG_PATH" --log-path "journald:" \
|
||||
--log-size-max "$log_size_max"
|
||||
assert_success
|
||||
[ -f "$LOG_PATH" ]
|
||||
}
|
||||
|
||||
@test "k8s log rotation: should create log file and accept small log size limits" {
|
||||
local log_size_max=100 # Very small to test edge cases
|
||||
run_conmon_k8s_file --log-size-max "$log_size_max"
|
||||
assert_success
|
||||
[ -f "$LOG_PATH" ]
|
||||
}
|
||||
|
||||
@test "k8s log rotation: should handle extremely small rotation limits without crashing" {
|
||||
local log_size_max=50 # Very small
|
||||
run_conmon_k8s_file --log-size-max "$log_size_max"
|
||||
assert_success
|
||||
[ -f "$LOG_PATH" ]
|
||||
}
|
||||
|
||||
@test "k8s log rotation: should properly validate log-size-max parameter bounds" {
|
||||
local test_cases=(1 10 100 1024 10240)
|
||||
|
||||
for size in "${test_cases[@]}"; do
|
||||
run_conmon_k8s_file --log-size-max "$size"
|
||||
assert_success
|
||||
[ -f "$LOG_PATH" ]
|
||||
|
||||
# Clean up log file for next iteration
|
||||
rm -f "$LOG_PATH"
|
||||
done
|
||||
}
|
||||
|
||||
@test "k8s log rotation: should create log files that can handle simulated k8s format content" {
|
||||
local log_size_max=1024 # Reasonable size for testing
|
||||
|
||||
run_conmon_k8s_file --log-size-max "$log_size_max"
|
||||
assert_success
|
||||
[ -f "$LOG_PATH" ]
|
||||
|
||||
# Simulate writing k8s format log entries to test the file is ready
|
||||
# This is what the fix addresses - proper log file state management
|
||||
local test_log_content='2023-07-23T18:00:00.000000000Z stdout F Log entry 1: Test message
|
||||
2023-07-23T18:00:01.000000000Z stdout F Log entry 2: Another test message
|
||||
2023-07-23T18:00:02.000000000Z stdout F Log entry 3: Final test message'
|
||||
|
||||
echo "$test_log_content" > "$LOG_PATH"
|
||||
|
||||
# Verify we can read back the content
|
||||
local content
|
||||
content=$(<"$LOG_PATH")
|
||||
[ "$content" = "$test_log_content" ]
|
||||
|
||||
# This test ensures the log file infrastructure works correctly
|
||||
# The actual fix prevents corruption when conmon handles the writev buffer
|
||||
# during log rotation, which would have caused malformed log entries
|
||||
}
|
||||
|
||||
@test "k8s log rotation: should handle zero log-size-max gracefully" {
|
||||
# Test with zero to ensure no division by zero or other edge case issues
|
||||
run_conmon_k8s_file --log-size-max 0
|
||||
# This might fail or succeed depending on implementation,
|
||||
# but should not crash
|
||||
# We just verify conmon doesn't crash
|
||||
[[ "$status" -eq 0 || "$status" -eq 1 ]]
|
||||
}
|
||||
|
||||
@test "k8s log rotation: should handle negative log-size-max gracefully" {
|
||||
# Test with negative value to ensure proper validation
|
||||
run_conmon_k8s_file --log-size-max -1
|
||||
# This should likely fail with validation error, but not crash
|
||||
[[ "$status" -eq 0 || "$status" -eq 1 ]]
|
||||
}
|
||||
|
||||
@test "k8s log rotation: should work with very large log-size-max" {
|
||||
local log_size_max=$((1024 * 1024 * 1024)) # 1GB
|
||||
run_conmon_k8s_file --log-size-max "$log_size_max"
|
||||
assert_success
|
||||
[ -f "$LOG_PATH" ]
|
||||
}
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
#!/usr/bin/env bats
|
||||
|
||||
load test_helper
|
||||
|
||||
setup() {
|
||||
check_conmon_binary
|
||||
check_runtime_binary
|
||||
setup_container_env
|
||||
}
|
||||
|
||||
teardown() {
|
||||
cleanup_test_env
|
||||
}
|
||||
|
||||
@test "runtime: simple runtime test" {
|
||||
# Run conmon which will create and manage the container
|
||||
# Using a timeout to prevent hanging
|
||||
timeout 30s "$CONMON_BINARY" \
|
||||
--cid "$CTR_ID" \
|
||||
--cuuid "$CTR_ID" \
|
||||
--runtime "$RUNTIME_BINARY" \
|
||||
--log-path "k8s-file:$LOG_PATH" \
|
||||
--bundle "$BUNDLE_PATH" \
|
||||
--socket-dir-path "$SOCKET_PATH" \
|
||||
--log-level debug \
|
||||
--container-pidfile "$PID_FILE" \
|
||||
--conmon-pidfile "$CONMON_PID_FILE" &
|
||||
|
||||
local conmon_pid=$!
|
||||
|
||||
# Give conmon time to start up and run the container
|
||||
sleep 2
|
||||
|
||||
# Check if conmon is still running or completed
|
||||
if kill -0 $conmon_pid 2>/dev/null; then
|
||||
# Kill conmon if it's still running
|
||||
kill $conmon_pid 2>/dev/null || true
|
||||
wait $conmon_pid 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Check that log file was created
|
||||
[ -f "$LOG_PATH" ]
|
||||
|
||||
# Check that conmon pidfile was created
|
||||
[ -f "$CONMON_PID_FILE" ]
|
||||
}
|
||||
|
||||
@test "runtime: container execution with different log drivers" {
|
||||
# Test with journald log driver
|
||||
timeout 30s "$CONMON_BINARY" \
|
||||
--cid "$CTR_ID" \
|
||||
--cuuid "$CTR_ID" \
|
||||
--runtime "$RUNTIME_BINARY" \
|
||||
--log-path "journald:" \
|
||||
--bundle "$BUNDLE_PATH" \
|
||||
--socket-dir-path "$SOCKET_PATH" \
|
||||
--container-pidfile "$PID_FILE" \
|
||||
--conmon-pidfile "$CONMON_PID_FILE" &
|
||||
|
||||
local conmon_pid=$!
|
||||
sleep 2
|
||||
|
||||
if kill -0 $conmon_pid 2>/dev/null; then
|
||||
kill $conmon_pid 2>/dev/null || true
|
||||
wait $conmon_pid 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Check that conmon pidfile was created
|
||||
[ -f "$CONMON_PID_FILE" ]
|
||||
}
|
||||
|
||||
@test "runtime: container execution with multiple log drivers" {
|
||||
# Test with both k8s-file and journald log drivers
|
||||
timeout 30s "$CONMON_BINARY" \
|
||||
--cid "$CTR_ID" \
|
||||
--cuuid "$CTR_ID" \
|
||||
--runtime "$RUNTIME_BINARY" \
|
||||
--log-path "k8s-file:$LOG_PATH" \
|
||||
--log-path "journald:" \
|
||||
--bundle "$BUNDLE_PATH" \
|
||||
--socket-dir-path "$SOCKET_PATH" \
|
||||
--container-pidfile "$PID_FILE" \
|
||||
--conmon-pidfile "$CONMON_PID_FILE" &
|
||||
|
||||
local conmon_pid=$!
|
||||
sleep 2
|
||||
|
||||
if kill -0 $conmon_pid 2>/dev/null; then
|
||||
kill $conmon_pid 2>/dev/null || true
|
||||
wait $conmon_pid 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Check that log file was created
|
||||
[ -f "$LOG_PATH" ]
|
||||
|
||||
# Check that conmon pidfile was created
|
||||
[ -f "$CONMON_PID_FILE" ]
|
||||
}
|
||||
|
||||
@test "runtime: container with log size limit" {
|
||||
# Test container execution with log rotation
|
||||
local log_size_max=1024
|
||||
|
||||
timeout 30s "$CONMON_BINARY" \
|
||||
--cid "$CTR_ID" \
|
||||
--cuuid "$CTR_ID" \
|
||||
--runtime "$RUNTIME_BINARY" \
|
||||
--log-path "k8s-file:$LOG_PATH" \
|
||||
--log-size-max "$log_size_max" \
|
||||
--bundle "$BUNDLE_PATH" \
|
||||
--socket-dir-path "$SOCKET_PATH" \
|
||||
--container-pidfile "$PID_FILE" \
|
||||
--conmon-pidfile "$CONMON_PID_FILE" &
|
||||
|
||||
local conmon_pid=$!
|
||||
sleep 2
|
||||
|
||||
if kill -0 $conmon_pid 2>/dev/null; then
|
||||
kill $conmon_pid 2>/dev/null || true
|
||||
wait $conmon_pid 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Check that log file was created
|
||||
[ -f "$LOG_PATH" ]
|
||||
|
||||
# Check that conmon pidfile was created
|
||||
[ -f "$CONMON_PID_FILE" ]
|
||||
}
|
||||
|
||||
@test "runtime: container cleanup on completion" {
|
||||
# Create and run a container, then verify cleanup
|
||||
timeout 30s "$CONMON_BINARY" \
|
||||
--cid "$CTR_ID" \
|
||||
--cuuid "$CTR_ID" \
|
||||
--runtime "$RUNTIME_BINARY" \
|
||||
--log-path "k8s-file:$LOG_PATH" \
|
||||
--bundle "$BUNDLE_PATH" \
|
||||
--socket-dir-path "$SOCKET_PATH" \
|
||||
--container-pidfile "$PID_FILE" \
|
||||
--conmon-pidfile "$CONMON_PID_FILE" &
|
||||
|
||||
local conmon_pid=$!
|
||||
sleep 2
|
||||
|
||||
if kill -0 $conmon_pid 2>/dev/null; then
|
||||
kill $conmon_pid 2>/dev/null || true
|
||||
wait $conmon_pid 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Check that log file was created
|
||||
[ -f "$LOG_PATH" ]
|
||||
|
||||
# Check that conmon pidfile was created
|
||||
[ -f "$CONMON_PID_FILE" ]
|
||||
}
|
||||
|
||||
@test "runtime: invalid runtime binary should fail" {
|
||||
# Test with non-existent runtime binary
|
||||
run_conmon \
|
||||
--cid "$CTR_ID" \
|
||||
--cuuid "$CTR_ID" \
|
||||
--runtime "/nonexistent/runtime" \
|
||||
--log-path "k8s-file:$LOG_PATH" \
|
||||
--bundle "$BUNDLE_PATH" \
|
||||
--socket-dir-path "$SOCKET_PATH" \
|
||||
--container-pidfile "$PID_FILE" \
|
||||
--conmon-pidfile "$CONMON_PID_FILE"
|
||||
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "runtime: configuration validation works" {
|
||||
# Test that conmon can validate its configuration
|
||||
# This is a basic smoke test for the runtime integration
|
||||
run_conmon --version
|
||||
assert_success
|
||||
assert_output_contains "conmon version"
|
||||
}
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Test runner script for conmon BATS tests
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# Default values
|
||||
CONMON_BINARY="${CONMON_BINARY:-$PROJECT_ROOT/bin/conmon}"
|
||||
RUNTIME_BINARY="${RUNTIME_BINARY:-/usr/bin/runc}"
|
||||
BATS_OPTIONS="${BATS_OPTIONS:-}"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
usage() {
|
||||
cat << EOF
|
||||
Usage: $0 [OPTIONS] [TEST_FILES...]
|
||||
|
||||
Run conmon BATS tests.
|
||||
|
||||
OPTIONS:
|
||||
-h, --help Show this help message
|
||||
-c, --conmon BINARY Path to conmon binary (default: $CONMON_BINARY)
|
||||
-r, --runtime BINARY Path to runtime binary (default: $RUNTIME_BINARY)
|
||||
-v, --verbose Verbose output
|
||||
-t, --tap Output in TAP format
|
||||
-j, --jobs N Run tests in parallel with N jobs
|
||||
--filter PATTERN Run only tests matching PATTERN
|
||||
|
||||
EXAMPLES:
|
||||
$0 Run all tests
|
||||
$0 01-basic.bats Run only basic tests
|
||||
$0 --verbose Run all tests with verbose output
|
||||
$0 --filter "version" Run only tests with 'version' in the name
|
||||
|
||||
ENVIRONMENT VARIABLES:
|
||||
CONMON_BINARY Path to conmon binary
|
||||
RUNTIME_BINARY Path to runtime binary
|
||||
BATS_OPTIONS Additional options to pass to bats
|
||||
EOF
|
||||
}
|
||||
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $*"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $*"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $*" >&2
|
||||
}
|
||||
|
||||
check_dependencies() {
|
||||
local missing_deps=()
|
||||
|
||||
if ! command -v bats >/dev/null 2>&1; then
|
||||
missing_deps+=("bats")
|
||||
fi
|
||||
|
||||
if [[ ! -x "$CONMON_BINARY" ]]; then
|
||||
missing_deps+=("conmon binary at $CONMON_BINARY")
|
||||
fi
|
||||
|
||||
if [[ ! -x "$RUNTIME_BINARY" ]]; then
|
||||
missing_deps+=("runtime binary at $RUNTIME_BINARY")
|
||||
fi
|
||||
|
||||
if [[ ${#missing_deps[@]} -gt 0 ]]; then
|
||||
log_error "Missing dependencies:"
|
||||
printf ' - %s\n' "${missing_deps[@]}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
local verbose=false
|
||||
local tap=false
|
||||
local jobs=""
|
||||
local filter=""
|
||||
local test_files=()
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
-c|--conmon)
|
||||
CONMON_BINARY="$2"
|
||||
shift 2
|
||||
;;
|
||||
-r|--runtime)
|
||||
RUNTIME_BINARY="$2"
|
||||
shift 2
|
||||
;;
|
||||
-v|--verbose)
|
||||
verbose=true
|
||||
shift
|
||||
;;
|
||||
-t|--tap)
|
||||
tap=true
|
||||
shift
|
||||
;;
|
||||
-j|--jobs)
|
||||
jobs="$2"
|
||||
shift 2
|
||||
;;
|
||||
--filter)
|
||||
filter="$2"
|
||||
shift 2
|
||||
;;
|
||||
*.bats)
|
||||
test_files+=("$1")
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown option: $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Set up BATS options
|
||||
local bats_args=()
|
||||
|
||||
if [[ "$verbose" == true ]]; then
|
||||
bats_args+=("--verbose-run")
|
||||
fi
|
||||
|
||||
if [[ "$tap" == true ]]; then
|
||||
bats_args+=("--tap")
|
||||
fi
|
||||
|
||||
if [[ -n "$jobs" ]]; then
|
||||
bats_args+=("--jobs" "$jobs")
|
||||
fi
|
||||
|
||||
if [[ -n "$filter" ]]; then
|
||||
bats_args+=("--filter" "$filter")
|
||||
fi
|
||||
|
||||
# Add any additional BATS options from environment
|
||||
if [[ -n "$BATS_OPTIONS" ]]; then
|
||||
read -ra additional_opts <<< "$BATS_OPTIONS"
|
||||
bats_args+=("${additional_opts[@]}")
|
||||
fi
|
||||
|
||||
# Check dependencies
|
||||
log_info "Checking dependencies..."
|
||||
if ! check_dependencies; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine test files to run
|
||||
if [[ ${#test_files[@]} -eq 0 ]]; then
|
||||
# Run all .bats files in test directory
|
||||
mapfile -t test_files < <(find "$SCRIPT_DIR" -name "*.bats" | sort)
|
||||
else
|
||||
# Convert relative paths to absolute paths
|
||||
local resolved_files=()
|
||||
for file in "${test_files[@]}"; do
|
||||
if [[ "$file" =~ ^/ ]]; then
|
||||
resolved_files+=("$file")
|
||||
elif [[ "$file" =~ test/ ]]; then
|
||||
# Already prefixed with test/, use from project root
|
||||
resolved_files+=("$PROJECT_ROOT/$file")
|
||||
else
|
||||
# Add test/ prefix
|
||||
resolved_files+=("$SCRIPT_DIR/$file")
|
||||
fi
|
||||
done
|
||||
test_files=("${resolved_files[@]}")
|
||||
fi
|
||||
|
||||
# Verify test files exist
|
||||
for file in "${test_files[@]}"; do
|
||||
if [[ ! -f "$file" ]]; then
|
||||
log_error "Test file not found: $file"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Export environment variables for tests
|
||||
export CONMON_BINARY
|
||||
export RUNTIME_BINARY
|
||||
|
||||
log_info "Running tests with:"
|
||||
log_info " conmon binary: $CONMON_BINARY"
|
||||
log_info " runtime binary: $RUNTIME_BINARY"
|
||||
log_info " test files: ${test_files[*]}"
|
||||
|
||||
# Run the tests
|
||||
log_info "Starting test execution..."
|
||||
if bats "${bats_args[@]}" "${test_files[@]}"; then
|
||||
log_info "All tests passed!"
|
||||
exit 0
|
||||
else
|
||||
log_error "Some tests failed!"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Only run main if script is executed directly
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
|
|
@ -0,0 +1,315 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Common test helper functions for conmon BATS tests
|
||||
|
||||
# Provide basic assertion functions if not available
|
||||
assert_success() {
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "Command failed with status $status"
|
||||
echo "Output: $output"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_failure() {
|
||||
if [ "$status" -eq 0 ]; then
|
||||
echo "Command succeeded but failure was expected"
|
||||
echo "Output: $output"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Default paths and variables
|
||||
CONMON_BINARY="${CONMON_BINARY:-/usr/bin/conmon}"
|
||||
RUNTIME_BINARY="${RUNTIME_BINARY:-/usr/bin/runc}"
|
||||
BUSYBOX_SOURCE="https://busybox.net/downloads/binaries/1.31.0-i686-uclibc/busybox"
|
||||
BUSYBOX_DEST_DIR="/tmp/conmon-test-images"
|
||||
BUSYBOX_DEST="/tmp/conmon-test-images/busybox"
|
||||
VALID_PATH="/tmp"
|
||||
INVALID_PATH="/not/a/path"
|
||||
|
||||
# Generate a unique container ID for each test
|
||||
generate_ctr_id() {
|
||||
echo "conmon-test-$(date +%s)-$$-$RANDOM"
|
||||
}
|
||||
|
||||
# Cache busybox binary for container tests
|
||||
cache_busybox() {
|
||||
if [[ -f "$BUSYBOX_DEST" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$BUSYBOX_DEST_DIR"
|
||||
if ! curl -s -L "$BUSYBOX_SOURCE" -o "$BUSYBOX_DEST"; then
|
||||
skip "Failed to download busybox binary"
|
||||
fi
|
||||
chmod +x "$BUSYBOX_DEST"
|
||||
}
|
||||
|
||||
# Run conmon with given arguments and capture output
|
||||
run_conmon() {
|
||||
run "$CONMON_BINARY" "$@"
|
||||
}
|
||||
|
||||
# Run runtime command (runc)
|
||||
run_runtime() {
|
||||
run "$RUNTIME_BINARY" "$@"
|
||||
}
|
||||
|
||||
# Get journal output for conmon process
|
||||
get_conmon_journal_output() {
|
||||
local pid="$1"
|
||||
local level="${2:--1}"
|
||||
|
||||
if ! command -v journalctl >/dev/null 2>&1; then
|
||||
echo ""
|
||||
return 0
|
||||
fi
|
||||
|
||||
local level_filter=""
|
||||
if [[ "$level" != "-1" ]]; then
|
||||
level_filter="-p $level"
|
||||
fi
|
||||
|
||||
journalctl -q --no-pager $level_filter _COMM=conmon _PID="$pid" 2>/dev/null || echo ""
|
||||
}
|
||||
|
||||
# Create a temporary directory for test
|
||||
setup_tmpdir() {
|
||||
export TEST_TMPDIR
|
||||
TEST_TMPDIR=$(mktemp -d /tmp/conmon-test-XXXXXX)
|
||||
}
|
||||
|
||||
# Cleanup temporary directory
|
||||
cleanup_tmpdir() {
|
||||
if [[ -n "$TEST_TMPDIR" ]]; then
|
||||
# Handle race condition where conmon might still be creating files
|
||||
local retries=5
|
||||
while [[ $retries -gt 0 ]]; do
|
||||
if rm -rf "$TEST_TMPDIR" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
((retries--))
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
# Generate OCI runtime configuration
|
||||
generate_runtime_config() {
|
||||
local bundle_path="$1"
|
||||
local rootfs="$2"
|
||||
local config_path="$bundle_path/config.json"
|
||||
|
||||
# Make rootfs path relative to bundle
|
||||
local relative_rootfs
|
||||
relative_rootfs=$(basename "$rootfs")
|
||||
|
||||
# Get current user UID and GID
|
||||
local host_uid host_gid
|
||||
host_uid=$(id -u)
|
||||
host_gid=$(id -g)
|
||||
|
||||
cat > "$config_path" << EOF
|
||||
{
|
||||
"ociVersion": "1.0.0",
|
||||
"process": {
|
||||
"terminal": false,
|
||||
"user": {
|
||||
"uid": 0,
|
||||
"gid": 0
|
||||
},
|
||||
"args": [
|
||||
"/busybox",
|
||||
"echo",
|
||||
"busybox"
|
||||
],
|
||||
"env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"cwd": "/",
|
||||
"capabilities": {
|
||||
"bounding": [],
|
||||
"effective": [],
|
||||
"inheritable": [],
|
||||
"permitted": [],
|
||||
"ambient": []
|
||||
},
|
||||
"rlimits": [
|
||||
{
|
||||
"type": "RLIMIT_NOFILE",
|
||||
"hard": 1024,
|
||||
"soft": 1024
|
||||
}
|
||||
],
|
||||
"noNewPrivileges": true
|
||||
},
|
||||
"root": {
|
||||
"path": "$relative_rootfs",
|
||||
"readonly": true
|
||||
},
|
||||
"hostname": "conmon-test",
|
||||
"mounts": [
|
||||
{
|
||||
"destination": "/proc",
|
||||
"type": "proc",
|
||||
"source": "proc"
|
||||
},
|
||||
{
|
||||
"destination": "/tmp",
|
||||
"type": "tmpfs",
|
||||
"source": "tmpfs",
|
||||
"options": [
|
||||
"nosuid",
|
||||
"nodev",
|
||||
"mode=1777"
|
||||
]
|
||||
}
|
||||
],
|
||||
"linux": {
|
||||
"resources": {
|
||||
"devices": [
|
||||
{
|
||||
"allow": false,
|
||||
"access": "rwm"
|
||||
}
|
||||
]
|
||||
},
|
||||
"namespaces": [
|
||||
{
|
||||
"type": "pid"
|
||||
},
|
||||
{
|
||||
"type": "ipc"
|
||||
},
|
||||
{
|
||||
"type": "uts"
|
||||
},
|
||||
{
|
||||
"type": "mount"
|
||||
},
|
||||
{
|
||||
"type": "user"
|
||||
}
|
||||
],
|
||||
"uidMappings": [
|
||||
{
|
||||
"containerID": 0,
|
||||
"hostID": $host_uid,
|
||||
"size": 1
|
||||
}
|
||||
],
|
||||
"gidMappings": [
|
||||
{
|
||||
"containerID": 0,
|
||||
"hostID": $host_gid,
|
||||
"size": 1
|
||||
}
|
||||
],
|
||||
"maskedPaths": [
|
||||
"/proc/acpi",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/proc/scsi",
|
||||
"/sys/firmware"
|
||||
],
|
||||
"readonlyPaths": [
|
||||
"/proc/asound",
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger"
|
||||
]
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Setup common test environment
|
||||
setup_test_env() {
|
||||
setup_tmpdir
|
||||
export CTR_ID
|
||||
CTR_ID=$(generate_ctr_id)
|
||||
export LOG_PATH="$TEST_TMPDIR/container.log"
|
||||
export PID_FILE="$TEST_TMPDIR/pidfile"
|
||||
export CONMON_PID_FILE="$TEST_TMPDIR/conmon-pidfile"
|
||||
export BUNDLE_PATH="$TEST_TMPDIR"
|
||||
export ROOTFS="$TEST_TMPDIR/rootfs"
|
||||
export SOCKET_PATH="$TEST_TMPDIR"
|
||||
}
|
||||
|
||||
# Setup full container environment with busybox
|
||||
setup_container_env() {
|
||||
setup_test_env
|
||||
|
||||
# Cache busybox binary for container tests
|
||||
cache_busybox
|
||||
|
||||
# Create the rootfs directory structure
|
||||
mkdir -p "$ROOTFS"/{bin,sbin,etc,proc,sys,dev,tmp}
|
||||
|
||||
# Copy busybox to rootfs and set up basic filesystem
|
||||
cp "$BUSYBOX_DEST" "$ROOTFS/busybox"
|
||||
chmod +x "$ROOTFS/busybox"
|
||||
|
||||
# Create busybox symlinks for common commands
|
||||
ln -sf busybox "$ROOTFS/bin/sh"
|
||||
ln -sf busybox "$ROOTFS/bin/echo"
|
||||
ln -sf busybox "$ROOTFS/bin/ls"
|
||||
ln -sf busybox "$ROOTFS/bin/cat"
|
||||
|
||||
# Create minimal /etc files
|
||||
echo "root:x:0:0:root:/:/bin/sh" > "$ROOTFS/etc/passwd"
|
||||
echo "root:x:0:" > "$ROOTFS/etc/group"
|
||||
|
||||
# Generate OCI runtime configuration
|
||||
generate_runtime_config "$BUNDLE_PATH" "$ROOTFS"
|
||||
}
|
||||
|
||||
# Cleanup test environment
|
||||
cleanup_test_env() {
|
||||
# Clean up any running containers
|
||||
if [[ -n "$CTR_ID" ]]; then
|
||||
"$RUNTIME_BINARY" delete -f "$CTR_ID" 2>/dev/null || true
|
||||
fi
|
||||
cleanup_tmpdir
|
||||
}
|
||||
|
||||
# Check if conmon binary exists and is executable
|
||||
check_conmon_binary() {
|
||||
if [[ ! -x "$CONMON_BINARY" ]]; then
|
||||
skip "conmon binary not found or not executable at $CONMON_BINARY"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if runtime binary exists and is executable
|
||||
check_runtime_binary() {
|
||||
if [[ ! -x "$RUNTIME_BINARY" ]]; then
|
||||
skip "runtime binary not found or not executable at $RUNTIME_BINARY"
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper to check if a string contains a substring
|
||||
assert_output_contains() {
|
||||
local expected="$1"
|
||||
if [[ "$output" != *"$expected"* ]]; then
|
||||
echo "Expected output to contain: $expected"
|
||||
echo "Actual output: $output"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper to check if stderr contains a substring
|
||||
assert_stderr_contains() {
|
||||
local expected="$1"
|
||||
if [[ "$stderr" != *"$expected"* ]]; then
|
||||
echo "Expected stderr to contain: $expected"
|
||||
echo "Actual stderr: $stderr"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
GO := go
|
||||
|
||||
BUILDDIR := build
|
||||
|
||||
all: $(BUILDDIR)
|
||||
|
||||
.PHONY: vendor
|
||||
vendor:
|
||||
export GO111MODULE=on \
|
||||
$(GO) mod tidy && \
|
||||
$(GO) mod vendor && \
|
||||
$(GO) mod verify
|
||||
|
||||
define go-build
|
||||
$(shell cd `pwd` && $(GO) build -o $(BUILDDIR)/$(shell basename $(1)) $(1))
|
||||
@echo > /dev/null
|
||||
endef
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)
|
||||
|
||||
$(BUILDDIR): \
|
||||
$(BUILDDIR)/go-md2man \
|
||||
|
||||
$(BUILDDIR)/go-md2man:
|
||||
$(call go-build,./vendor/github.com/cpuguy83/go-md2man)
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
module github.com/containers/storage/tests/tools
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/cpuguy83/go-md2man v1.0.10
|
||||
)
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
//go:build tools
|
||||
|
||||
package tools
|
||||
|
||||
// Importing the packages here will allow to vendor those via
|
||||
// `go mod vendor`.
|
||||
|
||||
import (
|
||||
_ "github.com/cpuguy83/go-md2man"
|
||||
)
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
go-md2man
|
||||
bin
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
language: go
|
||||
go:
|
||||
- "1.8.x"
|
||||
- "1.9.x"
|
||||
- "1.10.x"
|
||||
- "1.11.x"
|
||||
- "stable"
|
||||
- tip
|
||||
|
||||
env:
|
||||
- GO111MODULE: "on"
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
|
||||
script:
|
||||
- if [ "${TRAVIS_GO_VERSION}" = "stable" ]; then make check-mod; fi
|
||||
- if [ "${TRAVIS_GO_VERSION}" = "stable" ]; then make golangci-lint; fi
|
||||
- if [ "${TRAVIS_GO_VERSION}" = "stable" ]; then echo running check scripts; make check; fi
|
||||
- make build
|
||||
- make TEST_FLAGS="-v" test
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
FROM golang:1.8 AS build
|
||||
COPY . /go/src/github.com/cpuguy83/go-md2man
|
||||
WORKDIR /go/src/github.com/cpuguy83/go-md2man
|
||||
RUN CGO_ENABLED=0 go build
|
||||
|
||||
FROM scratch
|
||||
COPY --from=build /go/src/github.com/cpuguy83/go-md2man/go-md2man /go-md2man
|
||||
ENTRYPOINT ["/go-md2man"]
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Brian Goff
|
||||
|
||||
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.
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
GO111MODULE ?= on
|
||||
LINTER_BIN ?= golangci-lint
|
||||
|
||||
export GO111MODULE
|
||||
|
||||
.PHONY:
|
||||
build: bin/go-md2man
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@rm -rf bin/*
|
||||
|
||||
.PHONY: check
|
||||
check: lint
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@go test $(TEST_FLAGS) ./...
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@$(LINTER_BIN) run --new-from-rev "HEAD~$(git rev-list master.. --count)" ./...
|
||||
|
||||
bin/go-md2man: actual_build_flags := $(BUILD_FLAGS) -o bin/go-md2man
|
||||
bin/go-md2man: bin
|
||||
@CGO_ENABLED=0 go build $(actual_build_flags)
|
||||
|
||||
bin:
|
||||
@mkdir ./bin
|
||||
|
||||
$(LINTER_BIN): linter_bin_path := $(shell which $(LINTER_BIN))
|
||||
$(LINTER_BIN):
|
||||
@if [ -z $(linter_bin_path) ] || [ ! -x $(linter_bin_path) ]; then \
|
||||
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.15.0; \
|
||||
fi
|
||||
|
||||
.PHONY: mod
|
||||
mod:
|
||||
@go mod tidy
|
||||
|
||||
.PHONY: check-mod
|
||||
check-mod: # verifies that module changes for go.mod and go.sum are checked in
|
||||
@hack/ci/check_mods.sh
|
||||
|
||||
.PHONY: vendor
|
||||
vendor: mod
|
||||
@go mod vendor -v
|
||||
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
go-md2man
|
||||
=========
|
||||
|
||||
** Work in Progress **
|
||||
This still needs a lot of help to be complete, or even usable!
|
||||
|
||||
Uses blackfriday to process markdown into man pages.
|
||||
|
||||
### Usage
|
||||
|
||||
./md2man -in /path/to/markdownfile.md -out /manfile/output/path
|
||||
|
||||
### How to contribute
|
||||
|
||||
We use [dep](https://github.com/golang/dep/) for vendoring Go packages.
|
||||
See dep documentation for how to update.
|
||||
|
||||
### TODO
|
||||
|
||||
- Needs oh so much testing love
|
||||
- Look into blackfriday's 2.0 API
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
go-md2man 1 "January 2015" go-md2man "User Manual"
|
||||
==================================================
|
||||
|
||||
# NAME
|
||||
go-md2man - Convert mardown files into manpages
|
||||
|
||||
# SYNOPSIS
|
||||
go-md2man -in=[/path/to/md/file] -out=[/path/to/output]
|
||||
|
||||
# Description
|
||||
go-md2man converts standard markdown formatted documents into manpages. It is
|
||||
written purely in Go so as to reduce dependencies on 3rd party libs.
|
||||
|
||||
By default, the input is stdin and the output is stdout.
|
||||
|
||||
# Example
|
||||
Convert the markdown file "go-md2man.1.md" into a manpage.
|
||||
|
||||
go-md2man -in=README.md -out=go-md2man.1.out
|
||||
|
||||
# HISTORY
|
||||
January 2015, Originally compiled by Brian Goff( cpuguy83@gmail.com )
|
||||
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
module github.com/cpuguy83/go-md2man
|
||||
|
||||
go 1.12
|
||||
|
||||
require github.com/russross/blackfriday v1.5.2
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/cpuguy83/go-md2man/md2man"
|
||||
)
|
||||
|
||||
var inFilePath = flag.String("in", "", "Path to file to be processed (default: stdin)")
|
||||
var outFilePath = flag.String("out", "", "Path to output processed file (default: stdout)")
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
flag.Parse()
|
||||
|
||||
inFile := os.Stdin
|
||||
if *inFilePath != "" {
|
||||
inFile, err = os.Open(*inFilePath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
defer inFile.Close() // nolint: errcheck
|
||||
|
||||
doc, err := ioutil.ReadAll(inFile)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
out := md2man.Render(doc)
|
||||
|
||||
outFile := os.Stdout
|
||||
if *outFilePath != "" {
|
||||
outFile, err = os.Create(*outFilePath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer outFile.Close() // nolint: errcheck
|
||||
}
|
||||
_, err = outFile.Write(out)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package md2man
|
||||
|
||||
import (
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
// Render converts a markdown document into a roff formatted document.
|
||||
func Render(doc []byte) []byte {
|
||||
renderer := RoffRenderer(0)
|
||||
extensions := 0
|
||||
extensions |= blackfriday.EXTENSION_NO_INTRA_EMPHASIS
|
||||
extensions |= blackfriday.EXTENSION_TABLES
|
||||
extensions |= blackfriday.EXTENSION_FENCED_CODE
|
||||
extensions |= blackfriday.EXTENSION_AUTOLINK
|
||||
extensions |= blackfriday.EXTENSION_SPACE_HEADERS
|
||||
extensions |= blackfriday.EXTENSION_FOOTNOTES
|
||||
extensions |= blackfriday.EXTENSION_TITLEBLOCK
|
||||
|
||||
return blackfriday.Markdown(doc, renderer, extensions)
|
||||
}
|
||||
|
|
@ -1,285 +0,0 @@
|
|||
package md2man
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html"
|
||||
"strings"
|
||||
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
type roffRenderer struct {
|
||||
ListCounters []int
|
||||
}
|
||||
|
||||
// RoffRenderer creates a new blackfriday Renderer for generating roff documents
|
||||
// from markdown
|
||||
func RoffRenderer(flags int) blackfriday.Renderer {
|
||||
return &roffRenderer{}
|
||||
}
|
||||
|
||||
func (r *roffRenderer) GetFlags() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *roffRenderer) TitleBlock(out *bytes.Buffer, text []byte) {
|
||||
out.WriteString(".TH ")
|
||||
|
||||
splitText := bytes.Split(text, []byte("\n"))
|
||||
for i, line := range splitText {
|
||||
line = bytes.TrimPrefix(line, []byte("% "))
|
||||
if i == 0 {
|
||||
line = bytes.Replace(line, []byte("("), []byte("\" \""), 1)
|
||||
line = bytes.Replace(line, []byte(")"), []byte("\" \""), 1)
|
||||
}
|
||||
line = append([]byte("\""), line...)
|
||||
line = append(line, []byte("\" ")...)
|
||||
out.Write(line)
|
||||
}
|
||||
out.WriteString("\n")
|
||||
|
||||
// disable hyphenation
|
||||
out.WriteString(".nh\n")
|
||||
// disable justification (adjust text to left margin only)
|
||||
out.WriteString(".ad l\n")
|
||||
}
|
||||
|
||||
func (r *roffRenderer) BlockCode(out *bytes.Buffer, text []byte, lang string) {
|
||||
out.WriteString("\n.PP\n.RS\n\n.nf\n")
|
||||
escapeSpecialChars(out, text)
|
||||
out.WriteString("\n.fi\n.RE\n")
|
||||
}
|
||||
|
||||
func (r *roffRenderer) BlockQuote(out *bytes.Buffer, text []byte) {
|
||||
out.WriteString("\n.PP\n.RS\n")
|
||||
out.Write(text)
|
||||
out.WriteString("\n.RE\n")
|
||||
}
|
||||
|
||||
func (r *roffRenderer) BlockHtml(out *bytes.Buffer, text []byte) { // nolint: golint
|
||||
out.Write(text)
|
||||
}
|
||||
|
||||
func (r *roffRenderer) Header(out *bytes.Buffer, text func() bool, level int, id string) {
|
||||
marker := out.Len()
|
||||
|
||||
switch {
|
||||
case marker == 0:
|
||||
// This is the doc header
|
||||
out.WriteString(".TH ")
|
||||
case level == 1:
|
||||
out.WriteString("\n\n.SH ")
|
||||
case level == 2:
|
||||
out.WriteString("\n.SH ")
|
||||
default:
|
||||
out.WriteString("\n.SS ")
|
||||
}
|
||||
|
||||
if !text() {
|
||||
out.Truncate(marker)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *roffRenderer) HRule(out *bytes.Buffer) {
|
||||
out.WriteString("\n.ti 0\n\\l'\\n(.lu'\n")
|
||||
}
|
||||
|
||||
func (r *roffRenderer) List(out *bytes.Buffer, text func() bool, flags int) {
|
||||
marker := out.Len()
|
||||
r.ListCounters = append(r.ListCounters, 1)
|
||||
out.WriteString("\n.RS\n")
|
||||
if !text() {
|
||||
out.Truncate(marker)
|
||||
return
|
||||
}
|
||||
r.ListCounters = r.ListCounters[:len(r.ListCounters)-1]
|
||||
out.WriteString("\n.RE\n")
|
||||
}
|
||||
|
||||
func (r *roffRenderer) ListItem(out *bytes.Buffer, text []byte, flags int) {
|
||||
if flags&blackfriday.LIST_TYPE_ORDERED != 0 {
|
||||
out.WriteString(fmt.Sprintf(".IP \"%3d.\" 5\n", r.ListCounters[len(r.ListCounters)-1]))
|
||||
r.ListCounters[len(r.ListCounters)-1]++
|
||||
} else {
|
||||
out.WriteString(".IP \\(bu 2\n")
|
||||
}
|
||||
out.Write(text)
|
||||
out.WriteString("\n")
|
||||
}
|
||||
|
||||
func (r *roffRenderer) Paragraph(out *bytes.Buffer, text func() bool) {
|
||||
marker := out.Len()
|
||||
out.WriteString("\n.PP\n")
|
||||
if !text() {
|
||||
out.Truncate(marker)
|
||||
return
|
||||
}
|
||||
if marker != 0 {
|
||||
out.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *roffRenderer) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) {
|
||||
out.WriteString("\n.TS\nallbox;\n")
|
||||
|
||||
maxDelims := 0
|
||||
lines := strings.Split(strings.TrimRight(string(header), "\n")+"\n"+strings.TrimRight(string(body), "\n"), "\n")
|
||||
for _, w := range lines {
|
||||
curDelims := strings.Count(w, "\t")
|
||||
if curDelims > maxDelims {
|
||||
maxDelims = curDelims
|
||||
}
|
||||
}
|
||||
out.Write([]byte(strings.Repeat("l ", maxDelims+1) + "\n"))
|
||||
out.Write([]byte(strings.Repeat("l ", maxDelims+1) + ".\n"))
|
||||
out.Write(header)
|
||||
if len(header) > 0 {
|
||||
out.Write([]byte("\n"))
|
||||
}
|
||||
|
||||
out.Write(body)
|
||||
out.WriteString("\n.TE\n")
|
||||
}
|
||||
|
||||
func (r *roffRenderer) TableRow(out *bytes.Buffer, text []byte) {
|
||||
if out.Len() > 0 {
|
||||
out.WriteString("\n")
|
||||
}
|
||||
out.Write(text)
|
||||
}
|
||||
|
||||
func (r *roffRenderer) TableHeaderCell(out *bytes.Buffer, text []byte, align int) {
|
||||
if out.Len() > 0 {
|
||||
out.WriteString("\t")
|
||||
}
|
||||
if len(text) == 0 {
|
||||
text = []byte{' '}
|
||||
}
|
||||
out.Write([]byte("\\fB\\fC" + string(text) + "\\fR"))
|
||||
}
|
||||
|
||||
func (r *roffRenderer) TableCell(out *bytes.Buffer, text []byte, align int) {
|
||||
if out.Len() > 0 {
|
||||
out.WriteString("\t")
|
||||
}
|
||||
if len(text) > 30 {
|
||||
text = append([]byte("T{\n"), text...)
|
||||
text = append(text, []byte("\nT}")...)
|
||||
}
|
||||
if len(text) == 0 {
|
||||
text = []byte{' '}
|
||||
}
|
||||
out.Write(text)
|
||||
}
|
||||
|
||||
func (r *roffRenderer) Footnotes(out *bytes.Buffer, text func() bool) {
|
||||
|
||||
}
|
||||
|
||||
func (r *roffRenderer) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) {
|
||||
|
||||
}
|
||||
|
||||
func (r *roffRenderer) AutoLink(out *bytes.Buffer, link []byte, kind int) {
|
||||
out.WriteString("\n\\[la]")
|
||||
out.Write(link)
|
||||
out.WriteString("\\[ra]")
|
||||
}
|
||||
|
||||
func (r *roffRenderer) CodeSpan(out *bytes.Buffer, text []byte) {
|
||||
out.WriteString("\\fB\\fC")
|
||||
escapeSpecialChars(out, text)
|
||||
out.WriteString("\\fR")
|
||||
}
|
||||
|
||||
func (r *roffRenderer) DoubleEmphasis(out *bytes.Buffer, text []byte) {
|
||||
out.WriteString("\\fB")
|
||||
out.Write(text)
|
||||
out.WriteString("\\fP")
|
||||
}
|
||||
|
||||
func (r *roffRenderer) Emphasis(out *bytes.Buffer, text []byte) {
|
||||
out.WriteString("\\fI")
|
||||
out.Write(text)
|
||||
out.WriteString("\\fP")
|
||||
}
|
||||
|
||||
func (r *roffRenderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {
|
||||
}
|
||||
|
||||
func (r *roffRenderer) LineBreak(out *bytes.Buffer) {
|
||||
out.WriteString("\n.br\n")
|
||||
}
|
||||
|
||||
func (r *roffRenderer) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
|
||||
out.Write(content)
|
||||
r.AutoLink(out, link, 0)
|
||||
}
|
||||
|
||||
func (r *roffRenderer) RawHtmlTag(out *bytes.Buffer, tag []byte) { // nolint: golint
|
||||
out.Write(tag)
|
||||
}
|
||||
|
||||
func (r *roffRenderer) TripleEmphasis(out *bytes.Buffer, text []byte) {
|
||||
out.WriteString("\\s+2")
|
||||
out.Write(text)
|
||||
out.WriteString("\\s-2")
|
||||
}
|
||||
|
||||
func (r *roffRenderer) StrikeThrough(out *bytes.Buffer, text []byte) {
|
||||
}
|
||||
|
||||
func (r *roffRenderer) FootnoteRef(out *bytes.Buffer, ref []byte, id int) {
|
||||
|
||||
}
|
||||
|
||||
func (r *roffRenderer) Entity(out *bytes.Buffer, entity []byte) {
|
||||
out.WriteString(html.UnescapeString(string(entity)))
|
||||
}
|
||||
|
||||
func (r *roffRenderer) NormalText(out *bytes.Buffer, text []byte) {
|
||||
escapeSpecialChars(out, text)
|
||||
}
|
||||
|
||||
func (r *roffRenderer) DocumentHeader(out *bytes.Buffer) {
|
||||
}
|
||||
|
||||
func (r *roffRenderer) DocumentFooter(out *bytes.Buffer) {
|
||||
}
|
||||
|
||||
func needsBackslash(c byte) bool {
|
||||
for _, r := range []byte("-_&\\~") {
|
||||
if c == r {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func escapeSpecialChars(out *bytes.Buffer, text []byte) {
|
||||
for i := 0; i < len(text); i++ {
|
||||
// escape initial apostrophe or period
|
||||
if len(text) >= 1 && (text[0] == '\'' || text[0] == '.') {
|
||||
out.WriteString("\\&")
|
||||
}
|
||||
|
||||
// directly copy normal characters
|
||||
org := i
|
||||
|
||||
for i < len(text) && !needsBackslash(text[i]) {
|
||||
i++
|
||||
}
|
||||
if i > org {
|
||||
out.Write(text[org:i])
|
||||
}
|
||||
|
||||
// escape a character
|
||||
if i >= len(text) {
|
||||
break
|
||||
}
|
||||
out.WriteByte('\\')
|
||||
out.WriteByte(text[i])
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
*.out
|
||||
*.swp
|
||||
*.8
|
||||
*.6
|
||||
_obj
|
||||
_test*
|
||||
markdown
|
||||
tags
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
sudo: false
|
||||
language: go
|
||||
go:
|
||||
- "1.9.x"
|
||||
- "1.10.x"
|
||||
- tip
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- go: tip
|
||||
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 ./...
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
Blackfriday is distributed under the Simplified BSD License:
|
||||
|
||||
> Copyright © 2011 Russ Ross
|
||||
> All rights reserved.
|
||||
>
|
||||
> Redistribution and use in source and binary forms, with or without
|
||||
> modification, are permitted provided that the following conditions
|
||||
> are met:
|
||||
>
|
||||
> 1. Redistributions of source code must retain the above copyright
|
||||
> notice, this list of conditions and the following disclaimer.
|
||||
>
|
||||
> 2. Redistributions in binary form must reproduce the above
|
||||
> copyright notice, this list of conditions and the following
|
||||
> disclaimer in the documentation and/or other materials provided with
|
||||
> the distribution.
|
||||
>
|
||||
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
> "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
> LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
> FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
> COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
> INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
> BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
> LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
> ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
> POSSIBILITY OF SUCH DAMAGE.
|
||||
|
|
@ -1,369 +0,0 @@
|
|||
Blackfriday
|
||||
[![Build Status][BuildSVG]][BuildURL]
|
||||
[![Godoc][GodocV2SVG]][GodocV2URL]
|
||||
===========
|
||||
|
||||
Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It
|
||||
is paranoid about its input (so you can safely feed it user-supplied
|
||||
data), it is fast, it supports common extensions (tables, smart
|
||||
punctuation substitutions, etc.), and it is safe for all utf-8
|
||||
(unicode) input.
|
||||
|
||||
HTML output is currently supported, along with Smartypants
|
||||
extensions.
|
||||
|
||||
It started as a translation from C of [Sundown][3].
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Blackfriday is compatible with any modern Go release. With Go and git installed:
|
||||
|
||||
go get -u gopkg.in/russross/blackfriday.v2
|
||||
|
||||
will download, compile, and install the package into your `$GOPATH` directory
|
||||
hierarchy.
|
||||
|
||||
|
||||
Versions
|
||||
--------
|
||||
|
||||
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
|
||||
documentation is available at
|
||||
https://godoc.org/gopkg.in/russross/blackfriday.v2.
|
||||
|
||||
It is `go get`-able via [gopkg.in][6] at `gopkg.in/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:
|
||||
|
||||
* Cleaned up API
|
||||
* A separate call to [`Parse`][4], which produces an abstract syntax tree for
|
||||
the document
|
||||
* Latest bug fixes
|
||||
* Flexibility to easily add your own rendering extensions
|
||||
|
||||
Potential drawbacks:
|
||||
|
||||
* Our benchmarks show v2 to be slightly slower than v1. Currently in the
|
||||
ballpark of around 15%.
|
||||
* API breakage. If you can't afford modifying your code to adhere to the new API
|
||||
and don't care too much about the new features, v2 is probably not for you.
|
||||
* Several bug fixes are trailing behind and still need to be forward-ported to
|
||||
v2. See issue [#348](https://github.com/russross/blackfriday/issues/348) for
|
||||
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://godoc.org/github.com/russross/blackfriday
|
||||
|
||||
### Known issue with `dep`
|
||||
|
||||
There is a known problem with using Blackfriday v1 _transitively_ and `dep`.
|
||||
Currently `dep` prioritizes semver versions over anything else, and picks the
|
||||
latest one, plus it does not apply a `[[constraint]]` specifier to transitively
|
||||
pulled in packages. So if you're using something that uses Blackfriday v1, but
|
||||
that something does not use `dep` yet, you will get Blackfriday v2 pulled in and
|
||||
your first dependency will fail to build.
|
||||
|
||||
There are couple of fixes for it, documented here:
|
||||
https://github.com/golang/dep/blob/master/docs/FAQ.md#how-do-i-constrain-a-transitive-dependencys-version
|
||||
|
||||
Meanwhile, `dep` team is working on a more general solution to the constraints
|
||||
on transitive dependencies problem: https://github.com/golang/dep/issues/1124.
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
### v1
|
||||
|
||||
For basic usage, it is as simple as getting your input into a byte
|
||||
slice and calling:
|
||||
|
||||
output := blackfriday.MarkdownBasic(input)
|
||||
|
||||
This renders it with no extensions enabled. To get a more useful
|
||||
feature set, use this instead:
|
||||
|
||||
output := blackfriday.MarkdownCommon(input)
|
||||
|
||||
### v2
|
||||
|
||||
For the most sensible markdown processing, it is as simple as getting your input
|
||||
into a byte slice and calling:
|
||||
|
||||
```go
|
||||
output := blackfriday.Run(input)
|
||||
```
|
||||
|
||||
Your input will be parsed and the output rendered with a set of most popular
|
||||
extensions enabled. If you want the most basic feature set, corresponding with
|
||||
the bare Markdown specification, use:
|
||||
|
||||
```go
|
||||
output := blackfriday.Run(input, blackfriday.WithNoExtensions())
|
||||
```
|
||||
|
||||
### Sanitize untrusted content
|
||||
|
||||
Blackfriday itself does nothing to protect against malicious content. If you are
|
||||
dealing with user-supplied markdown, we recommend running Blackfriday's output
|
||||
through HTML sanitizer such as [Bluemonday][5].
|
||||
|
||||
Here's an example of simple usage of Blackfriday together with Bluemonday:
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"gopkg.in/russross/blackfriday.v2"
|
||||
)
|
||||
|
||||
// ...
|
||||
unsafe := blackfriday.Run(input)
|
||||
html := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
||||
```
|
||||
|
||||
### Custom options, v1
|
||||
|
||||
If you want to customize the set of options, first get a renderer
|
||||
(currently only the HTML output engine), then use it to
|
||||
call the more general `Markdown` function. For examples, see the
|
||||
implementations of `MarkdownBasic` and `MarkdownCommon` in
|
||||
`markdown.go`.
|
||||
|
||||
### Custom options, v2
|
||||
|
||||
If you want to customize the set of options, use `blackfriday.WithExtensions`,
|
||||
`blackfriday.WithRenderer` and `blackfriday.WithRefOverride`.
|
||||
|
||||
### `blackfriday-tool`
|
||||
|
||||
You can also check out `blackfriday-tool` for a more complete example
|
||||
of how to use it. Download and install it using:
|
||||
|
||||
go get github.com/russross/blackfriday-tool
|
||||
|
||||
This is a simple command-line tool that allows you to process a
|
||||
markdown file using a standalone program. You can also browse the
|
||||
source directly on github if you are just looking for some example
|
||||
code:
|
||||
|
||||
* <http://github.com/russross/blackfriday-tool>
|
||||
|
||||
Note that if you have not already done so, installing
|
||||
`blackfriday-tool` will be sufficient to download and install
|
||||
blackfriday in addition to the tool itself. The tool binary will be
|
||||
installed in `$GOPATH/bin`. This is a statically-linked binary that
|
||||
can be copied to wherever you need it without worrying about
|
||||
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 `EXTENSION_AUTO_HEADER_IDS` 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://godoc.org/github.com/russross/blackfriday#hdr-Sanitized_Anchor_Names.
|
||||
|
||||
[`SanitizedAnchorName`](https://godoc.org/github.com/russross/blackfriday#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://godoc.org/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
|
||||
--------
|
||||
|
||||
All features of Sundown are supported, including:
|
||||
|
||||
* **Compatibility**. The Markdown v1.0.3 test suite passes with
|
||||
the `--tidy` option. Without `--tidy`, the differences are
|
||||
mostly in whitespace and entity escaping, where blackfriday is
|
||||
more consistent and cleaner.
|
||||
|
||||
* **Common extensions**, including table support, fenced code
|
||||
blocks, autolinks, strikethroughs, non-strict emphasis, etc.
|
||||
|
||||
* **Safety**. Blackfriday is paranoid when parsing, making it safe
|
||||
to feed untrusted user input without fear of bad things
|
||||
happening. The test suite stress tests this and there are no
|
||||
known inputs that make it crash. If you find one, please let me
|
||||
know and send me the input that does it.
|
||||
|
||||
NOTE: "safety" in this context means *runtime safety only*. In order to
|
||||
protect yourself against JavaScript injection in untrusted content, see
|
||||
[this example](https://github.com/russross/blackfriday#sanitize-untrusted-content).
|
||||
|
||||
* **Fast processing**. It is fast enough to render on-demand in
|
||||
most web applications without having to cache the output.
|
||||
|
||||
* **Thread safety**. You can run multiple parsers in different
|
||||
goroutines without ill effect. There is no dependence on global
|
||||
shared state.
|
||||
|
||||
* **Minimal dependencies**. Blackfriday only depends on standard
|
||||
library packages in Go. The source code is pretty
|
||||
self-contained, so it is easy to add to any project, including
|
||||
Google App Engine projects.
|
||||
|
||||
* **Standards compliant**. Output successfully validates using the
|
||||
W3C validation tool for HTML 4.01 and XHTML 1.0 Transitional.
|
||||
|
||||
|
||||
Extensions
|
||||
----------
|
||||
|
||||
In addition to the standard markdown syntax, this package
|
||||
implements the following extensions:
|
||||
|
||||
* **Intra-word emphasis supression**. The `_` character is
|
||||
commonly used inside words when discussing code, so having
|
||||
markdown interpret it as an emphasis command is usually the
|
||||
wrong thing. Blackfriday lets you treat all emphasis markers as
|
||||
normal characters when they occur inside a word.
|
||||
|
||||
* **Tables**. Tables can be created by drawing them in the input
|
||||
using a simple syntax:
|
||||
|
||||
```
|
||||
Name | Age
|
||||
--------|------
|
||||
Bob | 27
|
||||
Alice | 23
|
||||
```
|
||||
|
||||
* **Fenced code blocks**. In addition to the normal 4-space
|
||||
indentation to mark code blocks, you can explicitly mark them
|
||||
and supply a language (to make syntax highlighting simple). Just
|
||||
mark it like this:
|
||||
|
||||
``` go
|
||||
func getTrue() bool {
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
term followed by a colon and the definition for that term.
|
||||
|
||||
Cat
|
||||
: Fluffy animal everyone likes
|
||||
|
||||
Internet
|
||||
: Vector of transmission for pictures of cats
|
||||
|
||||
Terms must be separated from the previous definition by a blank line.
|
||||
|
||||
* **Footnotes**. A marker in the text that will become a superscript number;
|
||||
a footnote definition that will be placed in a list of footnotes at the
|
||||
end of the document. A footnote looks like this:
|
||||
|
||||
This is a footnote.[^1]
|
||||
|
||||
[^1]: the footnote text.
|
||||
|
||||
* **Autolinking**. Blackfriday can find URLs that have not been
|
||||
explicitly marked as links and turn them into links.
|
||||
|
||||
* **Strikethrough**. Use two tildes (`~~`) to mark text that
|
||||
should be crossed out.
|
||||
|
||||
* **Hard line breaks**. With this extension enabled (it is off by
|
||||
default in the `MarkdownBasic` and `MarkdownCommon` convenience
|
||||
functions), newlines in the input translate into line breaks in
|
||||
the output.
|
||||
|
||||
* **Smart quotes**. Smartypants-style punctuation substitution is
|
||||
supported, turning normal double- and single-quote marks into
|
||||
curly quotes, etc.
|
||||
|
||||
* **LaTeX-style dash parsing** is an additional option, where `--`
|
||||
is translated into `–`, and `---` is translated into
|
||||
`—`. This differs from most smartypants processors, which
|
||||
turn a single hyphen into an ndash and a double hyphen into an
|
||||
mdash.
|
||||
|
||||
* **Smart fractions**, where anything that looks like a fraction
|
||||
is translated into suitable HTML (instead of just a few special
|
||||
cases like most smartypant processors). For example, `4/5`
|
||||
becomes `<sup>4</sup>⁄<sub>5</sub>`, which renders as
|
||||
<sup>4</sup>⁄<sub>5</sub>.
|
||||
|
||||
|
||||
Other renderers
|
||||
---------------
|
||||
|
||||
Blackfriday is structured to allow alternative rendering engines. Here
|
||||
are a few of note:
|
||||
|
||||
* [github_flavored_markdown](https://godoc.org/github.com/shurcooL/github_flavored_markdown):
|
||||
provides a GitHub Flavored Markdown renderer with fenced code block
|
||||
highlighting, clickable heading anchor links.
|
||||
|
||||
It's not customizable, and its goal is to produce HTML output
|
||||
equivalent to the [GitHub Markdown API endpoint](https://developer.github.com/v3/markdown/#render-a-markdown-document-in-raw-mode),
|
||||
except the rendering is performed locally.
|
||||
|
||||
* [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt,
|
||||
but for markdown.
|
||||
|
||||
* [LaTeX output](https://bitbucket.org/ambrevar/blackfriday-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.
|
||||
|
||||
|
||||
TODO
|
||||
----
|
||||
|
||||
* More unit testing
|
||||
* Improve Unicode support. It does not understand all Unicode
|
||||
rules (about what constitutes a letter, a punctuation symbol,
|
||||
etc.), so it may fail to detect word boundaries correctly in
|
||||
some instances. It is safe on all UTF-8 input.
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
[Blackfriday is distributed under the Simplified BSD License](LICENSE.txt)
|
||||
|
||||
|
||||
[1]: https://daringfireball.net/projects/markdown/ "Markdown"
|
||||
[2]: https://golang.org/ "Go Language"
|
||||
[3]: https://github.com/vmg/sundown "Sundown"
|
||||
[4]: https://godoc.org/gopkg.in/russross/blackfriday.v2#Parse "Parse func"
|
||||
[5]: https://github.com/microcosm-cc/bluemonday "Bluemonday"
|
||||
[6]: https://labix.org/gopkg.in "gopkg.in"
|
||||
[7]: https://github.com/golang/dep/ "dep"
|
||||
[8]: https://github.com/Masterminds/glide "Glide"
|
||||
|
||||
[BuildSVG]: https://travis-ci.org/russross/blackfriday.svg?branch=master
|
||||
[BuildURL]: https://travis-ci.org/russross/blackfriday
|
||||
[GodocV2SVG]: https://godoc.org/gopkg.in/russross/blackfriday.v2?status.svg
|
||||
[GodocV2URL]: https://godoc.org/gopkg.in/russross/blackfriday.v2
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,32 +0,0 @@
|
|||
// Package blackfriday is a Markdown processor.
|
||||
//
|
||||
// It translates plain text with simple formatting rules into HTML or LaTeX.
|
||||
//
|
||||
// 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 EXTENSION_AUTO_HEADER_IDS 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 preceed 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
|
||||
|
||||
// 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.
|
||||
|
|
@ -1 +0,0 @@
|
|||
module github.com/russross/blackfriday
|
||||
|
|
@ -1,938 +0,0 @@
|
|||
//
|
||||
// Blackfriday Markdown Processor
|
||||
// Available at http://github.com/russross/blackfriday
|
||||
//
|
||||
// Copyright © 2011 Russ Ross <russ@russross.com>.
|
||||
// Distributed under the Simplified BSD License.
|
||||
// See README.md for details.
|
||||
//
|
||||
|
||||
//
|
||||
//
|
||||
// HTML rendering backend
|
||||
//
|
||||
//
|
||||
|
||||
package blackfriday
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Html renderer configuration options.
|
||||
const (
|
||||
HTML_SKIP_HTML = 1 << iota // skip preformatted HTML blocks
|
||||
HTML_SKIP_STYLE // skip embedded <style> elements
|
||||
HTML_SKIP_IMAGES // skip embedded images
|
||||
HTML_SKIP_LINKS // skip all links
|
||||
HTML_SAFELINK // only link to trusted protocols
|
||||
HTML_NOFOLLOW_LINKS // only link with rel="nofollow"
|
||||
HTML_NOREFERRER_LINKS // only link with rel="noreferrer"
|
||||
HTML_HREF_TARGET_BLANK // add a blank target
|
||||
HTML_TOC // generate a table of contents
|
||||
HTML_OMIT_CONTENTS // skip the main contents (for a standalone table of contents)
|
||||
HTML_COMPLETE_PAGE // generate a complete HTML page
|
||||
HTML_USE_XHTML // generate XHTML output instead of HTML
|
||||
HTML_USE_SMARTYPANTS // enable smart punctuation substitutions
|
||||
HTML_SMARTYPANTS_FRACTIONS // enable smart fractions (with HTML_USE_SMARTYPANTS)
|
||||
HTML_SMARTYPANTS_DASHES // enable smart dashes (with HTML_USE_SMARTYPANTS)
|
||||
HTML_SMARTYPANTS_LATEX_DASHES // enable LaTeX-style dashes (with HTML_USE_SMARTYPANTS and HTML_SMARTYPANTS_DASHES)
|
||||
HTML_SMARTYPANTS_ANGLED_QUOTES // enable angled double quotes (with HTML_USE_SMARTYPANTS) for double quotes rendering
|
||||
HTML_SMARTYPANTS_QUOTES_NBSP // enable "French guillemets" (with HTML_USE_SMARTYPANTS)
|
||||
HTML_FOOTNOTE_RETURN_LINKS // generate a link at the end of a footnote to return to the source
|
||||
)
|
||||
|
||||
var (
|
||||
alignments = []string{
|
||||
"left",
|
||||
"right",
|
||||
"center",
|
||||
}
|
||||
|
||||
// TODO: improve this regexp to catch all possible entities:
|
||||
htmlEntity = regexp.MustCompile(`&[a-z]{2,5};`)
|
||||
)
|
||||
|
||||
type HtmlRendererParameters struct {
|
||||
// Prepend this text to each relative URL.
|
||||
AbsolutePrefix string
|
||||
// Add this text to each footnote anchor, to ensure uniqueness.
|
||||
FootnoteAnchorPrefix string
|
||||
// Show this text inside the <a> tag for a footnote return link, if the
|
||||
// HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string
|
||||
// <sup>[return]</sup> is used.
|
||||
FootnoteReturnLinkContents string
|
||||
// If set, add this text to the front of each Header ID, to ensure
|
||||
// uniqueness.
|
||||
HeaderIDPrefix string
|
||||
// If set, add this text to the back of each Header ID, to ensure uniqueness.
|
||||
HeaderIDSuffix string
|
||||
}
|
||||
|
||||
// Html is a type that implements the Renderer interface for HTML output.
|
||||
//
|
||||
// Do not create this directly, instead use the HtmlRenderer function.
|
||||
type Html struct {
|
||||
flags int // HTML_* options
|
||||
closeTag string // how to end singleton tags: either " />" or ">"
|
||||
title string // document title
|
||||
css string // optional css file url (used with HTML_COMPLETE_PAGE)
|
||||
|
||||
parameters HtmlRendererParameters
|
||||
|
||||
// table of contents data
|
||||
tocMarker int
|
||||
headerCount int
|
||||
currentLevel int
|
||||
toc *bytes.Buffer
|
||||
|
||||
// Track header IDs to prevent ID collision in a single generation.
|
||||
headerIDs map[string]int
|
||||
|
||||
smartypants *smartypantsRenderer
|
||||
}
|
||||
|
||||
const (
|
||||
xhtmlClose = " />"
|
||||
htmlClose = ">"
|
||||
)
|
||||
|
||||
// HtmlRenderer creates and configures an Html object, which
|
||||
// satisfies the Renderer interface.
|
||||
//
|
||||
// flags is a set of HTML_* options ORed together.
|
||||
// title is the title of the document, and css is a URL for the document's
|
||||
// stylesheet.
|
||||
// title and css are only used when HTML_COMPLETE_PAGE is selected.
|
||||
func HtmlRenderer(flags int, title string, css string) Renderer {
|
||||
return HtmlRendererWithParameters(flags, title, css, HtmlRendererParameters{})
|
||||
}
|
||||
|
||||
func HtmlRendererWithParameters(flags int, title string,
|
||||
css string, renderParameters HtmlRendererParameters) Renderer {
|
||||
// configure the rendering engine
|
||||
closeTag := htmlClose
|
||||
if flags&HTML_USE_XHTML != 0 {
|
||||
closeTag = xhtmlClose
|
||||
}
|
||||
|
||||
if renderParameters.FootnoteReturnLinkContents == "" {
|
||||
renderParameters.FootnoteReturnLinkContents = `<sup>[return]</sup>`
|
||||
}
|
||||
|
||||
return &Html{
|
||||
flags: flags,
|
||||
closeTag: closeTag,
|
||||
title: title,
|
||||
css: css,
|
||||
parameters: renderParameters,
|
||||
|
||||
headerCount: 0,
|
||||
currentLevel: 0,
|
||||
toc: new(bytes.Buffer),
|
||||
|
||||
headerIDs: make(map[string]int),
|
||||
|
||||
smartypants: smartypants(flags),
|
||||
}
|
||||
}
|
||||
|
||||
// Using if statements is a bit faster than a switch statement. As the compiler
|
||||
// improves, this should be unnecessary this is only worthwhile because
|
||||
// attrEscape is the single largest CPU user in normal use.
|
||||
// Also tried using map, but that gave a ~3x slowdown.
|
||||
func escapeSingleChar(char byte) (string, bool) {
|
||||
if char == '"' {
|
||||
return """, true
|
||||
}
|
||||
if char == '&' {
|
||||
return "&", true
|
||||
}
|
||||
if char == '<' {
|
||||
return "<", true
|
||||
}
|
||||
if char == '>' {
|
||||
return ">", true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func attrEscape(out *bytes.Buffer, src []byte) {
|
||||
org := 0
|
||||
for i, ch := range src {
|
||||
if entity, ok := escapeSingleChar(ch); ok {
|
||||
if i > org {
|
||||
// copy all the normal characters since the last escape
|
||||
out.Write(src[org:i])
|
||||
}
|
||||
org = i + 1
|
||||
out.WriteString(entity)
|
||||
}
|
||||
}
|
||||
if org < len(src) {
|
||||
out.Write(src[org:])
|
||||
}
|
||||
}
|
||||
|
||||
func entityEscapeWithSkip(out *bytes.Buffer, src []byte, skipRanges [][]int) {
|
||||
end := 0
|
||||
for _, rang := range skipRanges {
|
||||
attrEscape(out, src[end:rang[0]])
|
||||
out.Write(src[rang[0]:rang[1]])
|
||||
end = rang[1]
|
||||
}
|
||||
attrEscape(out, src[end:])
|
||||
}
|
||||
|
||||
func (options *Html) GetFlags() int {
|
||||
return options.flags
|
||||
}
|
||||
|
||||
func (options *Html) TitleBlock(out *bytes.Buffer, text []byte) {
|
||||
text = bytes.TrimPrefix(text, []byte("% "))
|
||||
text = bytes.Replace(text, []byte("\n% "), []byte("\n"), -1)
|
||||
out.WriteString("<h1 class=\"title\">")
|
||||
out.Write(text)
|
||||
out.WriteString("\n</h1>")
|
||||
}
|
||||
|
||||
func (options *Html) Header(out *bytes.Buffer, text func() bool, level int, id string) {
|
||||
marker := out.Len()
|
||||
doubleSpace(out)
|
||||
|
||||
if id == "" && options.flags&HTML_TOC != 0 {
|
||||
id = fmt.Sprintf("toc_%d", options.headerCount)
|
||||
}
|
||||
|
||||
if id != "" {
|
||||
id = options.ensureUniqueHeaderID(id)
|
||||
|
||||
if options.parameters.HeaderIDPrefix != "" {
|
||||
id = options.parameters.HeaderIDPrefix + id
|
||||
}
|
||||
|
||||
if options.parameters.HeaderIDSuffix != "" {
|
||||
id = id + options.parameters.HeaderIDSuffix
|
||||
}
|
||||
|
||||
out.WriteString(fmt.Sprintf("<h%d id=\"%s\">", level, id))
|
||||
} else {
|
||||
out.WriteString(fmt.Sprintf("<h%d>", level))
|
||||
}
|
||||
|
||||
tocMarker := out.Len()
|
||||
if !text() {
|
||||
out.Truncate(marker)
|
||||
return
|
||||
}
|
||||
|
||||
// are we building a table of contents?
|
||||
if options.flags&HTML_TOC != 0 {
|
||||
options.TocHeaderWithAnchor(out.Bytes()[tocMarker:], level, id)
|
||||
}
|
||||
|
||||
out.WriteString(fmt.Sprintf("</h%d>\n", level))
|
||||
}
|
||||
|
||||
func (options *Html) BlockHtml(out *bytes.Buffer, text []byte) {
|
||||
if options.flags&HTML_SKIP_HTML != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
doubleSpace(out)
|
||||
out.Write(text)
|
||||
out.WriteByte('\n')
|
||||
}
|
||||
|
||||
func (options *Html) HRule(out *bytes.Buffer) {
|
||||
doubleSpace(out)
|
||||
out.WriteString("<hr")
|
||||
out.WriteString(options.closeTag)
|
||||
out.WriteByte('\n')
|
||||
}
|
||||
|
||||
func (options *Html) BlockCode(out *bytes.Buffer, text []byte, info string) {
|
||||
doubleSpace(out)
|
||||
|
||||
endOfLang := strings.IndexAny(info, "\t ")
|
||||
if endOfLang < 0 {
|
||||
endOfLang = len(info)
|
||||
}
|
||||
lang := info[:endOfLang]
|
||||
if len(lang) == 0 || lang == "." {
|
||||
out.WriteString("<pre><code>")
|
||||
} else {
|
||||
out.WriteString("<pre><code class=\"language-")
|
||||
attrEscape(out, []byte(lang))
|
||||
out.WriteString("\">")
|
||||
}
|
||||
attrEscape(out, text)
|
||||
out.WriteString("</code></pre>\n")
|
||||
}
|
||||
|
||||
func (options *Html) BlockQuote(out *bytes.Buffer, text []byte) {
|
||||
doubleSpace(out)
|
||||
out.WriteString("<blockquote>\n")
|
||||
out.Write(text)
|
||||
out.WriteString("</blockquote>\n")
|
||||
}
|
||||
|
||||
func (options *Html) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) {
|
||||
doubleSpace(out)
|
||||
out.WriteString("<table>\n<thead>\n")
|
||||
out.Write(header)
|
||||
out.WriteString("</thead>\n\n<tbody>\n")
|
||||
out.Write(body)
|
||||
out.WriteString("</tbody>\n</table>\n")
|
||||
}
|
||||
|
||||
func (options *Html) TableRow(out *bytes.Buffer, text []byte) {
|
||||
doubleSpace(out)
|
||||
out.WriteString("<tr>\n")
|
||||
out.Write(text)
|
||||
out.WriteString("\n</tr>\n")
|
||||
}
|
||||
|
||||
func (options *Html) TableHeaderCell(out *bytes.Buffer, text []byte, align int) {
|
||||
doubleSpace(out)
|
||||
switch align {
|
||||
case TABLE_ALIGNMENT_LEFT:
|
||||
out.WriteString("<th align=\"left\">")
|
||||
case TABLE_ALIGNMENT_RIGHT:
|
||||
out.WriteString("<th align=\"right\">")
|
||||
case TABLE_ALIGNMENT_CENTER:
|
||||
out.WriteString("<th align=\"center\">")
|
||||
default:
|
||||
out.WriteString("<th>")
|
||||
}
|
||||
|
||||
out.Write(text)
|
||||
out.WriteString("</th>")
|
||||
}
|
||||
|
||||
func (options *Html) TableCell(out *bytes.Buffer, text []byte, align int) {
|
||||
doubleSpace(out)
|
||||
switch align {
|
||||
case TABLE_ALIGNMENT_LEFT:
|
||||
out.WriteString("<td align=\"left\">")
|
||||
case TABLE_ALIGNMENT_RIGHT:
|
||||
out.WriteString("<td align=\"right\">")
|
||||
case TABLE_ALIGNMENT_CENTER:
|
||||
out.WriteString("<td align=\"center\">")
|
||||
default:
|
||||
out.WriteString("<td>")
|
||||
}
|
||||
|
||||
out.Write(text)
|
||||
out.WriteString("</td>")
|
||||
}
|
||||
|
||||
func (options *Html) Footnotes(out *bytes.Buffer, text func() bool) {
|
||||
out.WriteString("<div class=\"footnotes\">\n")
|
||||
options.HRule(out)
|
||||
options.List(out, text, LIST_TYPE_ORDERED)
|
||||
out.WriteString("</div>\n")
|
||||
}
|
||||
|
||||
func (options *Html) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) {
|
||||
if flags&LIST_ITEM_CONTAINS_BLOCK != 0 || flags&LIST_ITEM_BEGINNING_OF_LIST != 0 {
|
||||
doubleSpace(out)
|
||||
}
|
||||
slug := slugify(name)
|
||||
out.WriteString(`<li id="`)
|
||||
out.WriteString(`fn:`)
|
||||
out.WriteString(options.parameters.FootnoteAnchorPrefix)
|
||||
out.Write(slug)
|
||||
out.WriteString(`">`)
|
||||
out.Write(text)
|
||||
if options.flags&HTML_FOOTNOTE_RETURN_LINKS != 0 {
|
||||
out.WriteString(` <a class="footnote-return" href="#`)
|
||||
out.WriteString(`fnref:`)
|
||||
out.WriteString(options.parameters.FootnoteAnchorPrefix)
|
||||
out.Write(slug)
|
||||
out.WriteString(`">`)
|
||||
out.WriteString(options.parameters.FootnoteReturnLinkContents)
|
||||
out.WriteString(`</a>`)
|
||||
}
|
||||
out.WriteString("</li>\n")
|
||||
}
|
||||
|
||||
func (options *Html) List(out *bytes.Buffer, text func() bool, flags int) {
|
||||
marker := out.Len()
|
||||
doubleSpace(out)
|
||||
|
||||
if flags&LIST_TYPE_DEFINITION != 0 {
|
||||
out.WriteString("<dl>")
|
||||
} else if flags&LIST_TYPE_ORDERED != 0 {
|
||||
out.WriteString("<ol>")
|
||||
} else {
|
||||
out.WriteString("<ul>")
|
||||
}
|
||||
if !text() {
|
||||
out.Truncate(marker)
|
||||
return
|
||||
}
|
||||
if flags&LIST_TYPE_DEFINITION != 0 {
|
||||
out.WriteString("</dl>\n")
|
||||
} else if flags&LIST_TYPE_ORDERED != 0 {
|
||||
out.WriteString("</ol>\n")
|
||||
} else {
|
||||
out.WriteString("</ul>\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (options *Html) ListItem(out *bytes.Buffer, text []byte, flags int) {
|
||||
if (flags&LIST_ITEM_CONTAINS_BLOCK != 0 && flags&LIST_TYPE_DEFINITION == 0) ||
|
||||
flags&LIST_ITEM_BEGINNING_OF_LIST != 0 {
|
||||
doubleSpace(out)
|
||||
}
|
||||
if flags&LIST_TYPE_TERM != 0 {
|
||||
out.WriteString("<dt>")
|
||||
} else if flags&LIST_TYPE_DEFINITION != 0 {
|
||||
out.WriteString("<dd>")
|
||||
} else {
|
||||
out.WriteString("<li>")
|
||||
}
|
||||
out.Write(text)
|
||||
if flags&LIST_TYPE_TERM != 0 {
|
||||
out.WriteString("</dt>\n")
|
||||
} else if flags&LIST_TYPE_DEFINITION != 0 {
|
||||
out.WriteString("</dd>\n")
|
||||
} else {
|
||||
out.WriteString("</li>\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (options *Html) Paragraph(out *bytes.Buffer, text func() bool) {
|
||||
marker := out.Len()
|
||||
doubleSpace(out)
|
||||
|
||||
out.WriteString("<p>")
|
||||
if !text() {
|
||||
out.Truncate(marker)
|
||||
return
|
||||
}
|
||||
out.WriteString("</p>\n")
|
||||
}
|
||||
|
||||
func (options *Html) AutoLink(out *bytes.Buffer, link []byte, kind int) {
|
||||
skipRanges := htmlEntity.FindAllIndex(link, -1)
|
||||
if options.flags&HTML_SAFELINK != 0 && !isSafeLink(link) && kind != LINK_TYPE_EMAIL {
|
||||
// mark it but don't link it if it is not a safe link: no smartypants
|
||||
out.WriteString("<tt>")
|
||||
entityEscapeWithSkip(out, link, skipRanges)
|
||||
out.WriteString("</tt>")
|
||||
return
|
||||
}
|
||||
|
||||
out.WriteString("<a href=\"")
|
||||
if kind == LINK_TYPE_EMAIL {
|
||||
out.WriteString("mailto:")
|
||||
} else {
|
||||
options.maybeWriteAbsolutePrefix(out, link)
|
||||
}
|
||||
|
||||
entityEscapeWithSkip(out, link, skipRanges)
|
||||
|
||||
var relAttrs []string
|
||||
if options.flags&HTML_NOFOLLOW_LINKS != 0 && !isRelativeLink(link) {
|
||||
relAttrs = append(relAttrs, "nofollow")
|
||||
}
|
||||
if options.flags&HTML_NOREFERRER_LINKS != 0 && !isRelativeLink(link) {
|
||||
relAttrs = append(relAttrs, "noreferrer")
|
||||
}
|
||||
if len(relAttrs) > 0 {
|
||||
out.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " ")))
|
||||
}
|
||||
|
||||
// blank target only add to external link
|
||||
if options.flags&HTML_HREF_TARGET_BLANK != 0 && !isRelativeLink(link) {
|
||||
out.WriteString("\" target=\"_blank")
|
||||
}
|
||||
|
||||
out.WriteString("\">")
|
||||
|
||||
// Pretty print: if we get an email address as
|
||||
// an actual URI, e.g. `mailto:foo@bar.com`, we don't
|
||||
// want to print the `mailto:` prefix
|
||||
switch {
|
||||
case bytes.HasPrefix(link, []byte("mailto://")):
|
||||
attrEscape(out, link[len("mailto://"):])
|
||||
case bytes.HasPrefix(link, []byte("mailto:")):
|
||||
attrEscape(out, link[len("mailto:"):])
|
||||
default:
|
||||
entityEscapeWithSkip(out, link, skipRanges)
|
||||
}
|
||||
|
||||
out.WriteString("</a>")
|
||||
}
|
||||
|
||||
func (options *Html) CodeSpan(out *bytes.Buffer, text []byte) {
|
||||
out.WriteString("<code>")
|
||||
attrEscape(out, text)
|
||||
out.WriteString("</code>")
|
||||
}
|
||||
|
||||
func (options *Html) DoubleEmphasis(out *bytes.Buffer, text []byte) {
|
||||
out.WriteString("<strong>")
|
||||
out.Write(text)
|
||||
out.WriteString("</strong>")
|
||||
}
|
||||
|
||||
func (options *Html) Emphasis(out *bytes.Buffer, text []byte) {
|
||||
if len(text) == 0 {
|
||||
return
|
||||
}
|
||||
out.WriteString("<em>")
|
||||
out.Write(text)
|
||||
out.WriteString("</em>")
|
||||
}
|
||||
|
||||
func (options *Html) maybeWriteAbsolutePrefix(out *bytes.Buffer, link []byte) {
|
||||
if options.parameters.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
|
||||
out.WriteString(options.parameters.AbsolutePrefix)
|
||||
if link[0] != '/' {
|
||||
out.WriteByte('/')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (options *Html) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {
|
||||
if options.flags&HTML_SKIP_IMAGES != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
out.WriteString("<img src=\"")
|
||||
options.maybeWriteAbsolutePrefix(out, link)
|
||||
attrEscape(out, link)
|
||||
out.WriteString("\" alt=\"")
|
||||
if len(alt) > 0 {
|
||||
attrEscape(out, alt)
|
||||
}
|
||||
if len(title) > 0 {
|
||||
out.WriteString("\" title=\"")
|
||||
attrEscape(out, title)
|
||||
}
|
||||
|
||||
out.WriteByte('"')
|
||||
out.WriteString(options.closeTag)
|
||||
}
|
||||
|
||||
func (options *Html) LineBreak(out *bytes.Buffer) {
|
||||
out.WriteString("<br")
|
||||
out.WriteString(options.closeTag)
|
||||
out.WriteByte('\n')
|
||||
}
|
||||
|
||||
func (options *Html) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
|
||||
if options.flags&HTML_SKIP_LINKS != 0 {
|
||||
// write the link text out but don't link it, just mark it with typewriter font
|
||||
out.WriteString("<tt>")
|
||||
attrEscape(out, content)
|
||||
out.WriteString("</tt>")
|
||||
return
|
||||
}
|
||||
|
||||
if options.flags&HTML_SAFELINK != 0 && !isSafeLink(link) {
|
||||
// write the link text out but don't link it, just mark it with typewriter font
|
||||
out.WriteString("<tt>")
|
||||
attrEscape(out, content)
|
||||
out.WriteString("</tt>")
|
||||
return
|
||||
}
|
||||
|
||||
out.WriteString("<a href=\"")
|
||||
options.maybeWriteAbsolutePrefix(out, link)
|
||||
attrEscape(out, link)
|
||||
if len(title) > 0 {
|
||||
out.WriteString("\" title=\"")
|
||||
attrEscape(out, title)
|
||||
}
|
||||
var relAttrs []string
|
||||
if options.flags&HTML_NOFOLLOW_LINKS != 0 && !isRelativeLink(link) {
|
||||
relAttrs = append(relAttrs, "nofollow")
|
||||
}
|
||||
if options.flags&HTML_NOREFERRER_LINKS != 0 && !isRelativeLink(link) {
|
||||
relAttrs = append(relAttrs, "noreferrer")
|
||||
}
|
||||
if len(relAttrs) > 0 {
|
||||
out.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " ")))
|
||||
}
|
||||
|
||||
// blank target only add to external link
|
||||
if options.flags&HTML_HREF_TARGET_BLANK != 0 && !isRelativeLink(link) {
|
||||
out.WriteString("\" target=\"_blank")
|
||||
}
|
||||
|
||||
out.WriteString("\">")
|
||||
out.Write(content)
|
||||
out.WriteString("</a>")
|
||||
return
|
||||
}
|
||||
|
||||
func (options *Html) RawHtmlTag(out *bytes.Buffer, text []byte) {
|
||||
if options.flags&HTML_SKIP_HTML != 0 {
|
||||
return
|
||||
}
|
||||
if options.flags&HTML_SKIP_STYLE != 0 && isHtmlTag(text, "style") {
|
||||
return
|
||||
}
|
||||
if options.flags&HTML_SKIP_LINKS != 0 && isHtmlTag(text, "a") {
|
||||
return
|
||||
}
|
||||
if options.flags&HTML_SKIP_IMAGES != 0 && isHtmlTag(text, "img") {
|
||||
return
|
||||
}
|
||||
out.Write(text)
|
||||
}
|
||||
|
||||
func (options *Html) TripleEmphasis(out *bytes.Buffer, text []byte) {
|
||||
out.WriteString("<strong><em>")
|
||||
out.Write(text)
|
||||
out.WriteString("</em></strong>")
|
||||
}
|
||||
|
||||
func (options *Html) StrikeThrough(out *bytes.Buffer, text []byte) {
|
||||
out.WriteString("<del>")
|
||||
out.Write(text)
|
||||
out.WriteString("</del>")
|
||||
}
|
||||
|
||||
func (options *Html) FootnoteRef(out *bytes.Buffer, ref []byte, id int) {
|
||||
slug := slugify(ref)
|
||||
out.WriteString(`<sup class="footnote-ref" id="`)
|
||||
out.WriteString(`fnref:`)
|
||||
out.WriteString(options.parameters.FootnoteAnchorPrefix)
|
||||
out.Write(slug)
|
||||
out.WriteString(`"><a href="#`)
|
||||
out.WriteString(`fn:`)
|
||||
out.WriteString(options.parameters.FootnoteAnchorPrefix)
|
||||
out.Write(slug)
|
||||
out.WriteString(`">`)
|
||||
out.WriteString(strconv.Itoa(id))
|
||||
out.WriteString(`</a></sup>`)
|
||||
}
|
||||
|
||||
func (options *Html) Entity(out *bytes.Buffer, entity []byte) {
|
||||
out.Write(entity)
|
||||
}
|
||||
|
||||
func (options *Html) NormalText(out *bytes.Buffer, text []byte) {
|
||||
if options.flags&HTML_USE_SMARTYPANTS != 0 {
|
||||
options.Smartypants(out, text)
|
||||
} else {
|
||||
attrEscape(out, text)
|
||||
}
|
||||
}
|
||||
|
||||
func (options *Html) Smartypants(out *bytes.Buffer, text []byte) {
|
||||
smrt := smartypantsData{false, false}
|
||||
|
||||
// first do normal entity escaping
|
||||
var escaped bytes.Buffer
|
||||
attrEscape(&escaped, text)
|
||||
text = escaped.Bytes()
|
||||
|
||||
mark := 0
|
||||
for i := 0; i < len(text); i++ {
|
||||
if action := options.smartypants[text[i]]; action != nil {
|
||||
if i > mark {
|
||||
out.Write(text[mark:i])
|
||||
}
|
||||
|
||||
previousChar := byte(0)
|
||||
if i > 0 {
|
||||
previousChar = text[i-1]
|
||||
}
|
||||
i += action(out, &smrt, previousChar, text[i:])
|
||||
mark = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
if mark < len(text) {
|
||||
out.Write(text[mark:])
|
||||
}
|
||||
}
|
||||
|
||||
func (options *Html) DocumentHeader(out *bytes.Buffer) {
|
||||
if options.flags&HTML_COMPLETE_PAGE == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ending := ""
|
||||
if options.flags&HTML_USE_XHTML != 0 {
|
||||
out.WriteString("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
|
||||
out.WriteString("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
|
||||
out.WriteString("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
|
||||
ending = " /"
|
||||
} else {
|
||||
out.WriteString("<!DOCTYPE html>\n")
|
||||
out.WriteString("<html>\n")
|
||||
}
|
||||
out.WriteString("<head>\n")
|
||||
out.WriteString(" <title>")
|
||||
options.NormalText(out, []byte(options.title))
|
||||
out.WriteString("</title>\n")
|
||||
out.WriteString(" <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
|
||||
out.WriteString(VERSION)
|
||||
out.WriteString("\"")
|
||||
out.WriteString(ending)
|
||||
out.WriteString(">\n")
|
||||
out.WriteString(" <meta charset=\"utf-8\"")
|
||||
out.WriteString(ending)
|
||||
out.WriteString(">\n")
|
||||
if options.css != "" {
|
||||
out.WriteString(" <link rel=\"stylesheet\" type=\"text/css\" href=\"")
|
||||
attrEscape(out, []byte(options.css))
|
||||
out.WriteString("\"")
|
||||
out.WriteString(ending)
|
||||
out.WriteString(">\n")
|
||||
}
|
||||
out.WriteString("</head>\n")
|
||||
out.WriteString("<body>\n")
|
||||
|
||||
options.tocMarker = out.Len()
|
||||
}
|
||||
|
||||
func (options *Html) DocumentFooter(out *bytes.Buffer) {
|
||||
// finalize and insert the table of contents
|
||||
if options.flags&HTML_TOC != 0 {
|
||||
options.TocFinalize()
|
||||
|
||||
// now we have to insert the table of contents into the document
|
||||
var temp bytes.Buffer
|
||||
|
||||
// start by making a copy of everything after the document header
|
||||
temp.Write(out.Bytes()[options.tocMarker:])
|
||||
|
||||
// now clear the copied material from the main output buffer
|
||||
out.Truncate(options.tocMarker)
|
||||
|
||||
// corner case spacing issue
|
||||
if options.flags&HTML_COMPLETE_PAGE != 0 {
|
||||
out.WriteByte('\n')
|
||||
}
|
||||
|
||||
// insert the table of contents
|
||||
out.WriteString("<nav>\n")
|
||||
out.Write(options.toc.Bytes())
|
||||
out.WriteString("</nav>\n")
|
||||
|
||||
// corner case spacing issue
|
||||
if options.flags&HTML_COMPLETE_PAGE == 0 && options.flags&HTML_OMIT_CONTENTS == 0 {
|
||||
out.WriteByte('\n')
|
||||
}
|
||||
|
||||
// write out everything that came after it
|
||||
if options.flags&HTML_OMIT_CONTENTS == 0 {
|
||||
out.Write(temp.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
if options.flags&HTML_COMPLETE_PAGE != 0 {
|
||||
out.WriteString("\n</body>\n")
|
||||
out.WriteString("</html>\n")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (options *Html) TocHeaderWithAnchor(text []byte, level int, anchor string) {
|
||||
for level > options.currentLevel {
|
||||
switch {
|
||||
case bytes.HasSuffix(options.toc.Bytes(), []byte("</li>\n")):
|
||||
// this sublist can nest underneath a header
|
||||
size := options.toc.Len()
|
||||
options.toc.Truncate(size - len("</li>\n"))
|
||||
|
||||
case options.currentLevel > 0:
|
||||
options.toc.WriteString("<li>")
|
||||
}
|
||||
if options.toc.Len() > 0 {
|
||||
options.toc.WriteByte('\n')
|
||||
}
|
||||
options.toc.WriteString("<ul>\n")
|
||||
options.currentLevel++
|
||||
}
|
||||
|
||||
for level < options.currentLevel {
|
||||
options.toc.WriteString("</ul>")
|
||||
if options.currentLevel > 1 {
|
||||
options.toc.WriteString("</li>\n")
|
||||
}
|
||||
options.currentLevel--
|
||||
}
|
||||
|
||||
options.toc.WriteString("<li><a href=\"#")
|
||||
if anchor != "" {
|
||||
options.toc.WriteString(anchor)
|
||||
} else {
|
||||
options.toc.WriteString("toc_")
|
||||
options.toc.WriteString(strconv.Itoa(options.headerCount))
|
||||
}
|
||||
options.toc.WriteString("\">")
|
||||
options.headerCount++
|
||||
|
||||
options.toc.Write(text)
|
||||
|
||||
options.toc.WriteString("</a></li>\n")
|
||||
}
|
||||
|
||||
func (options *Html) TocHeader(text []byte, level int) {
|
||||
options.TocHeaderWithAnchor(text, level, "")
|
||||
}
|
||||
|
||||
func (options *Html) TocFinalize() {
|
||||
for options.currentLevel > 1 {
|
||||
options.toc.WriteString("</ul></li>\n")
|
||||
options.currentLevel--
|
||||
}
|
||||
|
||||
if options.currentLevel > 0 {
|
||||
options.toc.WriteString("</ul>\n")
|
||||
}
|
||||
}
|
||||
|
||||
func isHtmlTag(tag []byte, tagname string) bool {
|
||||
found, _ := findHtmlTagPos(tag, tagname)
|
||||
return found
|
||||
}
|
||||
|
||||
// Look for a character, but ignore it when it's in any kind of quotes, it
|
||||
// might be JavaScript
|
||||
func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int {
|
||||
inSingleQuote := false
|
||||
inDoubleQuote := false
|
||||
inGraveQuote := false
|
||||
i := start
|
||||
for i < len(html) {
|
||||
switch {
|
||||
case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote:
|
||||
return i
|
||||
case html[i] == '\'':
|
||||
inSingleQuote = !inSingleQuote
|
||||
case html[i] == '"':
|
||||
inDoubleQuote = !inDoubleQuote
|
||||
case html[i] == '`':
|
||||
inGraveQuote = !inGraveQuote
|
||||
}
|
||||
i++
|
||||
}
|
||||
return start
|
||||
}
|
||||
|
||||
func findHtmlTagPos(tag []byte, tagname string) (bool, int) {
|
||||
i := 0
|
||||
if i < len(tag) && tag[0] != '<' {
|
||||
return false, -1
|
||||
}
|
||||
i++
|
||||
i = skipSpace(tag, i)
|
||||
|
||||
if i < len(tag) && tag[i] == '/' {
|
||||
i++
|
||||
}
|
||||
|
||||
i = skipSpace(tag, i)
|
||||
j := 0
|
||||
for ; i < len(tag); i, j = i+1, j+1 {
|
||||
if j >= len(tagname) {
|
||||
break
|
||||
}
|
||||
|
||||
if strings.ToLower(string(tag[i]))[0] != tagname[j] {
|
||||
return false, -1
|
||||
}
|
||||
}
|
||||
|
||||
if i == len(tag) {
|
||||
return false, -1
|
||||
}
|
||||
|
||||
rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
|
||||
if rightAngle > i {
|
||||
return true, rightAngle
|
||||
}
|
||||
|
||||
return false, -1
|
||||
}
|
||||
|
||||
func skipUntilChar(text []byte, start int, char byte) int {
|
||||
i := start
|
||||
for i < len(text) && text[i] != char {
|
||||
i++
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func skipSpace(tag []byte, i int) int {
|
||||
for i < len(tag) && isspace(tag[i]) {
|
||||
i++
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func skipChar(data []byte, start int, char byte) int {
|
||||
i := start
|
||||
for i < len(data) && data[i] == char {
|
||||
i++
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func doubleSpace(out *bytes.Buffer) {
|
||||
if out.Len() > 0 {
|
||||
out.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
func isRelativeLink(link []byte) (yes bool) {
|
||||
// a tag begin with '#'
|
||||
if link[0] == '#' {
|
||||
return true
|
||||
}
|
||||
|
||||
// link begin with '/' but not '//', the second maybe a protocol relative link
|
||||
if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
|
||||
return true
|
||||
}
|
||||
|
||||
// only the root '/'
|
||||
if len(link) == 1 && link[0] == '/' {
|
||||
return true
|
||||
}
|
||||
|
||||
// current directory : begin with "./"
|
||||
if bytes.HasPrefix(link, []byte("./")) {
|
||||
return true
|
||||
}
|
||||
|
||||
// parent directory : begin with "../"
|
||||
if bytes.HasPrefix(link, []byte("../")) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (options *Html) ensureUniqueHeaderID(id string) string {
|
||||
for count, found := options.headerIDs[id]; found; count, found = options.headerIDs[id] {
|
||||
tmp := fmt.Sprintf("%s-%d", id, count+1)
|
||||
|
||||
if _, tmpFound := options.headerIDs[tmp]; !tmpFound {
|
||||
options.headerIDs[id] = count + 1
|
||||
id = tmp
|
||||
} else {
|
||||
id = id + "-1"
|
||||
}
|
||||
}
|
||||
|
||||
if _, found := options.headerIDs[id]; !found {
|
||||
options.headerIDs[id] = 0
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,334 +0,0 @@
|
|||
//
|
||||
// Blackfriday Markdown Processor
|
||||
// Available at http://github.com/russross/blackfriday
|
||||
//
|
||||
// Copyright © 2011 Russ Ross <russ@russross.com>.
|
||||
// Distributed under the Simplified BSD License.
|
||||
// See README.md for details.
|
||||
//
|
||||
|
||||
//
|
||||
//
|
||||
// LaTeX rendering backend
|
||||
//
|
||||
//
|
||||
|
||||
package blackfriday
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Latex is a type that implements the Renderer interface for LaTeX output.
|
||||
//
|
||||
// Do not create this directly, instead use the LatexRenderer function.
|
||||
type Latex struct {
|
||||
}
|
||||
|
||||
// LatexRenderer creates and configures a Latex object, which
|
||||
// satisfies the Renderer interface.
|
||||
//
|
||||
// flags is a set of LATEX_* options ORed together (currently no such options
|
||||
// are defined).
|
||||
func LatexRenderer(flags int) Renderer {
|
||||
return &Latex{}
|
||||
}
|
||||
|
||||
func (options *Latex) GetFlags() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// render code chunks using verbatim, or listings if we have a language
|
||||
func (options *Latex) BlockCode(out *bytes.Buffer, text []byte, info string) {
|
||||
if info == "" {
|
||||
out.WriteString("\n\\begin{verbatim}\n")
|
||||
} else {
|
||||
lang := strings.Fields(info)[0]
|
||||
out.WriteString("\n\\begin{lstlisting}[language=")
|
||||
out.WriteString(lang)
|
||||
out.WriteString("]\n")
|
||||
}
|
||||
out.Write(text)
|
||||
if info == "" {
|
||||
out.WriteString("\n\\end{verbatim}\n")
|
||||
} else {
|
||||
out.WriteString("\n\\end{lstlisting}\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (options *Latex) TitleBlock(out *bytes.Buffer, text []byte) {
|
||||
|
||||
}
|
||||
|
||||
func (options *Latex) BlockQuote(out *bytes.Buffer, text []byte) {
|
||||
out.WriteString("\n\\begin{quotation}\n")
|
||||
out.Write(text)
|
||||
out.WriteString("\n\\end{quotation}\n")
|
||||
}
|
||||
|
||||
func (options *Latex) BlockHtml(out *bytes.Buffer, text []byte) {
|
||||
// a pretty lame thing to do...
|
||||
out.WriteString("\n\\begin{verbatim}\n")
|
||||
out.Write(text)
|
||||
out.WriteString("\n\\end{verbatim}\n")
|
||||
}
|
||||
|
||||
func (options *Latex) Header(out *bytes.Buffer, text func() bool, level int, id string) {
|
||||
marker := out.Len()
|
||||
|
||||
switch level {
|
||||
case 1:
|
||||
out.WriteString("\n\\section{")
|
||||
case 2:
|
||||
out.WriteString("\n\\subsection{")
|
||||
case 3:
|
||||
out.WriteString("\n\\subsubsection{")
|
||||
case 4:
|
||||
out.WriteString("\n\\paragraph{")
|
||||
case 5:
|
||||
out.WriteString("\n\\subparagraph{")
|
||||
case 6:
|
||||
out.WriteString("\n\\textbf{")
|
||||
}
|
||||
if !text() {
|
||||
out.Truncate(marker)
|
||||
return
|
||||
}
|
||||
out.WriteString("}\n")
|
||||
}
|
||||
|
||||
func (options *Latex) HRule(out *bytes.Buffer) {
|
||||
out.WriteString("\n\\HRule\n")
|
||||
}
|
||||
|
||||
func (options *Latex) List(out *bytes.Buffer, text func() bool, flags int) {
|
||||
marker := out.Len()
|
||||
if flags&LIST_TYPE_ORDERED != 0 {
|
||||
out.WriteString("\n\\begin{enumerate}\n")
|
||||
} else {
|
||||
out.WriteString("\n\\begin{itemize}\n")
|
||||
}
|
||||
if !text() {
|
||||
out.Truncate(marker)
|
||||
return
|
||||
}
|
||||
if flags&LIST_TYPE_ORDERED != 0 {
|
||||
out.WriteString("\n\\end{enumerate}\n")
|
||||
} else {
|
||||
out.WriteString("\n\\end{itemize}\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (options *Latex) ListItem(out *bytes.Buffer, text []byte, flags int) {
|
||||
out.WriteString("\n\\item ")
|
||||
out.Write(text)
|
||||
}
|
||||
|
||||
func (options *Latex) Paragraph(out *bytes.Buffer, text func() bool) {
|
||||
marker := out.Len()
|
||||
out.WriteString("\n")
|
||||
if !text() {
|
||||
out.Truncate(marker)
|
||||
return
|
||||
}
|
||||
out.WriteString("\n")
|
||||
}
|
||||
|
||||
func (options *Latex) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) {
|
||||
out.WriteString("\n\\begin{tabular}{")
|
||||
for _, elt := range columnData {
|
||||
switch elt {
|
||||
case TABLE_ALIGNMENT_LEFT:
|
||||
out.WriteByte('l')
|
||||
case TABLE_ALIGNMENT_RIGHT:
|
||||
out.WriteByte('r')
|
||||
default:
|
||||
out.WriteByte('c')
|
||||
}
|
||||
}
|
||||
out.WriteString("}\n")
|
||||
out.Write(header)
|
||||
out.WriteString(" \\\\\n\\hline\n")
|
||||
out.Write(body)
|
||||
out.WriteString("\n\\end{tabular}\n")
|
||||
}
|
||||
|
||||
func (options *Latex) TableRow(out *bytes.Buffer, text []byte) {
|
||||
if out.Len() > 0 {
|
||||
out.WriteString(" \\\\\n")
|
||||
}
|
||||
out.Write(text)
|
||||
}
|
||||
|
||||
func (options *Latex) TableHeaderCell(out *bytes.Buffer, text []byte, align int) {
|
||||
if out.Len() > 0 {
|
||||
out.WriteString(" & ")
|
||||
}
|
||||
out.Write(text)
|
||||
}
|
||||
|
||||
func (options *Latex) TableCell(out *bytes.Buffer, text []byte, align int) {
|
||||
if out.Len() > 0 {
|
||||
out.WriteString(" & ")
|
||||
}
|
||||
out.Write(text)
|
||||
}
|
||||
|
||||
// TODO: this
|
||||
func (options *Latex) Footnotes(out *bytes.Buffer, text func() bool) {
|
||||
|
||||
}
|
||||
|
||||
func (options *Latex) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) {
|
||||
|
||||
}
|
||||
|
||||
func (options *Latex) AutoLink(out *bytes.Buffer, link []byte, kind int) {
|
||||
out.WriteString("\\href{")
|
||||
if kind == LINK_TYPE_EMAIL {
|
||||
out.WriteString("mailto:")
|
||||
}
|
||||
out.Write(link)
|
||||
out.WriteString("}{")
|
||||
out.Write(link)
|
||||
out.WriteString("}")
|
||||
}
|
||||
|
||||
func (options *Latex) CodeSpan(out *bytes.Buffer, text []byte) {
|
||||
out.WriteString("\\texttt{")
|
||||
escapeSpecialChars(out, text)
|
||||
out.WriteString("}")
|
||||
}
|
||||
|
||||
func (options *Latex) DoubleEmphasis(out *bytes.Buffer, text []byte) {
|
||||
out.WriteString("\\textbf{")
|
||||
out.Write(text)
|
||||
out.WriteString("}")
|
||||
}
|
||||
|
||||
func (options *Latex) Emphasis(out *bytes.Buffer, text []byte) {
|
||||
out.WriteString("\\textit{")
|
||||
out.Write(text)
|
||||
out.WriteString("}")
|
||||
}
|
||||
|
||||
func (options *Latex) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {
|
||||
if bytes.HasPrefix(link, []byte("http://")) || bytes.HasPrefix(link, []byte("https://")) {
|
||||
// treat it like a link
|
||||
out.WriteString("\\href{")
|
||||
out.Write(link)
|
||||
out.WriteString("}{")
|
||||
out.Write(alt)
|
||||
out.WriteString("}")
|
||||
} else {
|
||||
out.WriteString("\\includegraphics{")
|
||||
out.Write(link)
|
||||
out.WriteString("}")
|
||||
}
|
||||
}
|
||||
|
||||
func (options *Latex) LineBreak(out *bytes.Buffer) {
|
||||
out.WriteString(" \\\\\n")
|
||||
}
|
||||
|
||||
func (options *Latex) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
|
||||
out.WriteString("\\href{")
|
||||
out.Write(link)
|
||||
out.WriteString("}{")
|
||||
out.Write(content)
|
||||
out.WriteString("}")
|
||||
}
|
||||
|
||||
func (options *Latex) RawHtmlTag(out *bytes.Buffer, tag []byte) {
|
||||
}
|
||||
|
||||
func (options *Latex) TripleEmphasis(out *bytes.Buffer, text []byte) {
|
||||
out.WriteString("\\textbf{\\textit{")
|
||||
out.Write(text)
|
||||
out.WriteString("}}")
|
||||
}
|
||||
|
||||
func (options *Latex) StrikeThrough(out *bytes.Buffer, text []byte) {
|
||||
out.WriteString("\\sout{")
|
||||
out.Write(text)
|
||||
out.WriteString("}")
|
||||
}
|
||||
|
||||
// TODO: this
|
||||
func (options *Latex) FootnoteRef(out *bytes.Buffer, ref []byte, id int) {
|
||||
|
||||
}
|
||||
|
||||
func needsBackslash(c byte) bool {
|
||||
for _, r := range []byte("_{}%$&\\~#") {
|
||||
if c == r {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func escapeSpecialChars(out *bytes.Buffer, text []byte) {
|
||||
for i := 0; i < len(text); i++ {
|
||||
// directly copy normal characters
|
||||
org := i
|
||||
|
||||
for i < len(text) && !needsBackslash(text[i]) {
|
||||
i++
|
||||
}
|
||||
if i > org {
|
||||
out.Write(text[org:i])
|
||||
}
|
||||
|
||||
// escape a character
|
||||
if i >= len(text) {
|
||||
break
|
||||
}
|
||||
out.WriteByte('\\')
|
||||
out.WriteByte(text[i])
|
||||
}
|
||||
}
|
||||
|
||||
func (options *Latex) Entity(out *bytes.Buffer, entity []byte) {
|
||||
// TODO: convert this into a unicode character or something
|
||||
out.Write(entity)
|
||||
}
|
||||
|
||||
func (options *Latex) NormalText(out *bytes.Buffer, text []byte) {
|
||||
escapeSpecialChars(out, text)
|
||||
}
|
||||
|
||||
// header and footer
|
||||
func (options *Latex) DocumentHeader(out *bytes.Buffer) {
|
||||
out.WriteString("\\documentclass{article}\n")
|
||||
out.WriteString("\n")
|
||||
out.WriteString("\\usepackage{graphicx}\n")
|
||||
out.WriteString("\\usepackage{listings}\n")
|
||||
out.WriteString("\\usepackage[margin=1in]{geometry}\n")
|
||||
out.WriteString("\\usepackage[utf8]{inputenc}\n")
|
||||
out.WriteString("\\usepackage{verbatim}\n")
|
||||
out.WriteString("\\usepackage[normalem]{ulem}\n")
|
||||
out.WriteString("\\usepackage{hyperref}\n")
|
||||
out.WriteString("\n")
|
||||
out.WriteString("\\hypersetup{colorlinks,%\n")
|
||||
out.WriteString(" citecolor=black,%\n")
|
||||
out.WriteString(" filecolor=black,%\n")
|
||||
out.WriteString(" linkcolor=black,%\n")
|
||||
out.WriteString(" urlcolor=black,%\n")
|
||||
out.WriteString(" pdfstartview=FitH,%\n")
|
||||
out.WriteString(" breaklinks=true,%\n")
|
||||
out.WriteString(" pdfauthor={Blackfriday Markdown Processor v")
|
||||
out.WriteString(VERSION)
|
||||
out.WriteString("}}\n")
|
||||
out.WriteString("\n")
|
||||
out.WriteString("\\newcommand{\\HRule}{\\rule{\\linewidth}{0.5mm}}\n")
|
||||
out.WriteString("\\addtolength{\\parskip}{0.5\\baselineskip}\n")
|
||||
out.WriteString("\\parindent=0pt\n")
|
||||
out.WriteString("\n")
|
||||
out.WriteString("\\begin{document}\n")
|
||||
}
|
||||
|
||||
func (options *Latex) DocumentFooter(out *bytes.Buffer) {
|
||||
out.WriteString("\n\\end{document}\n")
|
||||
}
|
||||
|
|
@ -1,941 +0,0 @@
|
|||
//
|
||||
// Blackfriday Markdown Processor
|
||||
// Available at http://github.com/russross/blackfriday
|
||||
//
|
||||
// Copyright © 2011 Russ Ross <russ@russross.com>.
|
||||
// Distributed under the Simplified BSD License.
|
||||
// See README.md for details.
|
||||
//
|
||||
|
||||
//
|
||||
//
|
||||
// Markdown parsing and processing
|
||||
//
|
||||
//
|
||||
|
||||
package blackfriday
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const VERSION = "1.5"
|
||||
|
||||
// These are the supported markdown parsing extensions.
|
||||
// OR these values together to select multiple extensions.
|
||||
const (
|
||||
EXTENSION_NO_INTRA_EMPHASIS = 1 << iota // ignore emphasis markers inside words
|
||||
EXTENSION_TABLES // render tables
|
||||
EXTENSION_FENCED_CODE // render fenced code blocks
|
||||
EXTENSION_AUTOLINK // detect embedded URLs that are not explicitly marked
|
||||
EXTENSION_STRIKETHROUGH // strikethrough text using ~~test~~
|
||||
EXTENSION_LAX_HTML_BLOCKS // loosen up HTML block parsing rules
|
||||
EXTENSION_SPACE_HEADERS // be strict about prefix header rules
|
||||
EXTENSION_HARD_LINE_BREAK // translate newlines into line breaks
|
||||
EXTENSION_TAB_SIZE_EIGHT // expand tabs to eight spaces instead of four
|
||||
EXTENSION_FOOTNOTES // Pandoc-style footnotes
|
||||
EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK // No need to insert an empty line to start a (code, quote, ordered list, unordered list) block
|
||||
EXTENSION_HEADER_IDS // specify header IDs with {#id}
|
||||
EXTENSION_TITLEBLOCK // Titleblock ala pandoc
|
||||
EXTENSION_AUTO_HEADER_IDS // Create the header ID from the text
|
||||
EXTENSION_BACKSLASH_LINE_BREAK // translate trailing backslashes into line breaks
|
||||
EXTENSION_DEFINITION_LISTS // render definition lists
|
||||
EXTENSION_JOIN_LINES // delete newline and join lines
|
||||
|
||||
commonHtmlFlags = 0 |
|
||||
HTML_USE_XHTML |
|
||||
HTML_USE_SMARTYPANTS |
|
||||
HTML_SMARTYPANTS_FRACTIONS |
|
||||
HTML_SMARTYPANTS_DASHES |
|
||||
HTML_SMARTYPANTS_LATEX_DASHES
|
||||
|
||||
commonExtensions = 0 |
|
||||
EXTENSION_NO_INTRA_EMPHASIS |
|
||||
EXTENSION_TABLES |
|
||||
EXTENSION_FENCED_CODE |
|
||||
EXTENSION_AUTOLINK |
|
||||
EXTENSION_STRIKETHROUGH |
|
||||
EXTENSION_SPACE_HEADERS |
|
||||
EXTENSION_HEADER_IDS |
|
||||
EXTENSION_BACKSLASH_LINE_BREAK |
|
||||
EXTENSION_DEFINITION_LISTS
|
||||
)
|
||||
|
||||
// These are the possible flag values for the link renderer.
|
||||
// Only a single one of these values will be used; they are not ORed together.
|
||||
// These are mostly of interest if you are writing a new output format.
|
||||
const (
|
||||
LINK_TYPE_NOT_AUTOLINK = iota
|
||||
LINK_TYPE_NORMAL
|
||||
LINK_TYPE_EMAIL
|
||||
)
|
||||
|
||||
// These are the possible flag values for the ListItem renderer.
|
||||
// Multiple flag values may be ORed together.
|
||||
// These are mostly of interest if you are writing a new output format.
|
||||
const (
|
||||
LIST_TYPE_ORDERED = 1 << iota
|
||||
LIST_TYPE_DEFINITION
|
||||
LIST_TYPE_TERM
|
||||
LIST_ITEM_CONTAINS_BLOCK
|
||||
LIST_ITEM_BEGINNING_OF_LIST
|
||||
LIST_ITEM_END_OF_LIST
|
||||
)
|
||||
|
||||
// These are the possible flag values for the table cell renderer.
|
||||
// Only a single one of these values will be used; they are not ORed together.
|
||||
// These are mostly of interest if you are writing a new output format.
|
||||
const (
|
||||
TABLE_ALIGNMENT_LEFT = 1 << iota
|
||||
TABLE_ALIGNMENT_RIGHT
|
||||
TABLE_ALIGNMENT_CENTER = (TABLE_ALIGNMENT_LEFT | TABLE_ALIGNMENT_RIGHT)
|
||||
)
|
||||
|
||||
// The size of a tab stop.
|
||||
const (
|
||||
TAB_SIZE_DEFAULT = 4
|
||||
TAB_SIZE_EIGHT = 8
|
||||
)
|
||||
|
||||
// blockTags is a set of tags that are recognized as HTML block tags.
|
||||
// Any of these can be included in markdown text without special escaping.
|
||||
var blockTags = map[string]struct{}{
|
||||
"blockquote": {},
|
||||
"del": {},
|
||||
"div": {},
|
||||
"dl": {},
|
||||
"fieldset": {},
|
||||
"form": {},
|
||||
"h1": {},
|
||||
"h2": {},
|
||||
"h3": {},
|
||||
"h4": {},
|
||||
"h5": {},
|
||||
"h6": {},
|
||||
"iframe": {},
|
||||
"ins": {},
|
||||
"math": {},
|
||||
"noscript": {},
|
||||
"ol": {},
|
||||
"pre": {},
|
||||
"p": {},
|
||||
"script": {},
|
||||
"style": {},
|
||||
"table": {},
|
||||
"ul": {},
|
||||
|
||||
// HTML5
|
||||
"address": {},
|
||||
"article": {},
|
||||
"aside": {},
|
||||
"canvas": {},
|
||||
"figcaption": {},
|
||||
"figure": {},
|
||||
"footer": {},
|
||||
"header": {},
|
||||
"hgroup": {},
|
||||
"main": {},
|
||||
"nav": {},
|
||||
"output": {},
|
||||
"progress": {},
|
||||
"section": {},
|
||||
"video": {},
|
||||
}
|
||||
|
||||
// Renderer is the rendering interface.
|
||||
// This is mostly of interest if you are implementing a new rendering format.
|
||||
//
|
||||
// When a byte slice is provided, it contains the (rendered) contents of the
|
||||
// element.
|
||||
//
|
||||
// When a callback is provided instead, it will write the contents of the
|
||||
// respective element directly to the output buffer and return true on success.
|
||||
// If the callback returns false, the rendering function should reset the
|
||||
// output buffer as though it had never been called.
|
||||
//
|
||||
// Currently Html and Latex implementations are provided
|
||||
type Renderer interface {
|
||||
// block-level callbacks
|
||||
BlockCode(out *bytes.Buffer, text []byte, infoString string)
|
||||
BlockQuote(out *bytes.Buffer, text []byte)
|
||||
BlockHtml(out *bytes.Buffer, text []byte)
|
||||
Header(out *bytes.Buffer, text func() bool, level int, id string)
|
||||
HRule(out *bytes.Buffer)
|
||||
List(out *bytes.Buffer, text func() bool, flags int)
|
||||
ListItem(out *bytes.Buffer, text []byte, flags int)
|
||||
Paragraph(out *bytes.Buffer, text func() bool)
|
||||
Table(out *bytes.Buffer, header []byte, body []byte, columnData []int)
|
||||
TableRow(out *bytes.Buffer, text []byte)
|
||||
TableHeaderCell(out *bytes.Buffer, text []byte, flags int)
|
||||
TableCell(out *bytes.Buffer, text []byte, flags int)
|
||||
Footnotes(out *bytes.Buffer, text func() bool)
|
||||
FootnoteItem(out *bytes.Buffer, name, text []byte, flags int)
|
||||
TitleBlock(out *bytes.Buffer, text []byte)
|
||||
|
||||
// Span-level callbacks
|
||||
AutoLink(out *bytes.Buffer, link []byte, kind int)
|
||||
CodeSpan(out *bytes.Buffer, text []byte)
|
||||
DoubleEmphasis(out *bytes.Buffer, text []byte)
|
||||
Emphasis(out *bytes.Buffer, text []byte)
|
||||
Image(out *bytes.Buffer, link []byte, title []byte, alt []byte)
|
||||
LineBreak(out *bytes.Buffer)
|
||||
Link(out *bytes.Buffer, link []byte, title []byte, content []byte)
|
||||
RawHtmlTag(out *bytes.Buffer, tag []byte)
|
||||
TripleEmphasis(out *bytes.Buffer, text []byte)
|
||||
StrikeThrough(out *bytes.Buffer, text []byte)
|
||||
FootnoteRef(out *bytes.Buffer, ref []byte, id int)
|
||||
|
||||
// Low-level callbacks
|
||||
Entity(out *bytes.Buffer, entity []byte)
|
||||
NormalText(out *bytes.Buffer, text []byte)
|
||||
|
||||
// Header and footer
|
||||
DocumentHeader(out *bytes.Buffer)
|
||||
DocumentFooter(out *bytes.Buffer)
|
||||
|
||||
GetFlags() int
|
||||
}
|
||||
|
||||
// Callback functions for inline parsing. One such function is defined
|
||||
// for each character that triggers a response when parsing inline data.
|
||||
type inlineParser func(p *parser, out *bytes.Buffer, data []byte, offset int) int
|
||||
|
||||
// Parser holds runtime state used by the parser.
|
||||
// This is constructed by the Markdown function.
|
||||
type parser struct {
|
||||
r Renderer
|
||||
refOverride ReferenceOverrideFunc
|
||||
refs map[string]*reference
|
||||
inlineCallback [256]inlineParser
|
||||
flags int
|
||||
nesting int
|
||||
maxNesting int
|
||||
insideLink bool
|
||||
|
||||
// Footnotes need to be ordered as well as available to quickly check for
|
||||
// presence. If a ref is also a footnote, it's stored both in refs and here
|
||||
// in notes. Slice is nil if footnotes not enabled.
|
||||
notes []*reference
|
||||
notesRecord map[string]struct{}
|
||||
}
|
||||
|
||||
func (p *parser) getRef(refid string) (ref *reference, found bool) {
|
||||
if p.refOverride != nil {
|
||||
r, overridden := p.refOverride(refid)
|
||||
if overridden {
|
||||
if r == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &reference{
|
||||
link: []byte(r.Link),
|
||||
title: []byte(r.Title),
|
||||
noteId: 0,
|
||||
hasBlock: false,
|
||||
text: []byte(r.Text)}, true
|
||||
}
|
||||
}
|
||||
// refs are case insensitive
|
||||
ref, found = p.refs[strings.ToLower(refid)]
|
||||
return ref, found
|
||||
}
|
||||
|
||||
func (p *parser) isFootnote(ref *reference) bool {
|
||||
_, ok := p.notesRecord[string(ref.link)]
|
||||
return ok
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
// Public interface
|
||||
//
|
||||
//
|
||||
|
||||
// Reference represents the details of a link.
|
||||
// See the documentation in Options for more details on use-case.
|
||||
type Reference struct {
|
||||
// Link is usually the URL the reference points to.
|
||||
Link string
|
||||
// Title is the alternate text describing the link in more detail.
|
||||
Title string
|
||||
// Text is the optional text to override the ref with if the syntax used was
|
||||
// [refid][]
|
||||
Text string
|
||||
}
|
||||
|
||||
// ReferenceOverrideFunc is expected to be called with a reference string and
|
||||
// return either a valid Reference type that the reference string maps to or
|
||||
// nil. If overridden is false, the default reference logic will be executed.
|
||||
// See the documentation in Options for more details on use-case.
|
||||
type ReferenceOverrideFunc func(reference string) (ref *Reference, overridden bool)
|
||||
|
||||
// Options represents configurable overrides and callbacks (in addition to the
|
||||
// extension flag set) for configuring a Markdown parse.
|
||||
type Options struct {
|
||||
// Extensions is a flag set of bit-wise ORed extension bits. See the
|
||||
// EXTENSION_* flags defined in this package.
|
||||
Extensions int
|
||||
|
||||
// ReferenceOverride is an optional function callback that is called every
|
||||
// time a reference is resolved.
|
||||
//
|
||||
// In Markdown, the link reference syntax can be made to resolve a link to
|
||||
// a reference instead of an inline URL, in one of the following ways:
|
||||
//
|
||||
// * [link text][refid]
|
||||
// * [refid][]
|
||||
//
|
||||
// Usually, the refid is defined at the bottom of the Markdown document. If
|
||||
// this override function is provided, the refid is passed to the override
|
||||
// function first, before consulting the defined refids at the bottom. If
|
||||
// the override function indicates an override did not occur, the refids at
|
||||
// the bottom will be used to fill in the link details.
|
||||
ReferenceOverride ReferenceOverrideFunc
|
||||
}
|
||||
|
||||
// MarkdownBasic is a convenience function for simple rendering.
|
||||
// It processes markdown input with no extensions enabled.
|
||||
func MarkdownBasic(input []byte) []byte {
|
||||
// set up the HTML renderer
|
||||
htmlFlags := HTML_USE_XHTML
|
||||
renderer := HtmlRenderer(htmlFlags, "", "")
|
||||
|
||||
// set up the parser
|
||||
return MarkdownOptions(input, renderer, Options{Extensions: 0})
|
||||
}
|
||||
|
||||
// Call Markdown with most useful extensions enabled
|
||||
// MarkdownCommon is a convenience function for simple rendering.
|
||||
// It processes markdown input with common extensions enabled, including:
|
||||
//
|
||||
// * Smartypants processing with smart fractions and LaTeX dashes
|
||||
//
|
||||
// * Intra-word emphasis suppression
|
||||
//
|
||||
// * Tables
|
||||
//
|
||||
// * Fenced code blocks
|
||||
//
|
||||
// * Autolinking
|
||||
//
|
||||
// * Strikethrough support
|
||||
//
|
||||
// * Strict header parsing
|
||||
//
|
||||
// * Custom Header IDs
|
||||
func MarkdownCommon(input []byte) []byte {
|
||||
// set up the HTML renderer
|
||||
renderer := HtmlRenderer(commonHtmlFlags, "", "")
|
||||
return MarkdownOptions(input, renderer, Options{
|
||||
Extensions: commonExtensions})
|
||||
}
|
||||
|
||||
// Markdown is the main rendering function.
|
||||
// It parses and renders a block of markdown-encoded text.
|
||||
// The supplied Renderer is used to format the output, and extensions dictates
|
||||
// which non-standard extensions are enabled.
|
||||
//
|
||||
// To use the supplied Html or LaTeX renderers, see HtmlRenderer and
|
||||
// LatexRenderer, respectively.
|
||||
func Markdown(input []byte, renderer Renderer, extensions int) []byte {
|
||||
return MarkdownOptions(input, renderer, Options{
|
||||
Extensions: extensions})
|
||||
}
|
||||
|
||||
// MarkdownOptions is just like Markdown but takes additional options through
|
||||
// the Options struct.
|
||||
func MarkdownOptions(input []byte, renderer Renderer, opts Options) []byte {
|
||||
// no point in parsing if we can't render
|
||||
if renderer == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
extensions := opts.Extensions
|
||||
|
||||
// fill in the render structure
|
||||
p := new(parser)
|
||||
p.r = renderer
|
||||
p.flags = extensions
|
||||
p.refOverride = opts.ReferenceOverride
|
||||
p.refs = make(map[string]*reference)
|
||||
p.maxNesting = 16
|
||||
p.insideLink = false
|
||||
|
||||
// register inline parsers
|
||||
p.inlineCallback['*'] = emphasis
|
||||
p.inlineCallback['_'] = emphasis
|
||||
if extensions&EXTENSION_STRIKETHROUGH != 0 {
|
||||
p.inlineCallback['~'] = emphasis
|
||||
}
|
||||
p.inlineCallback['`'] = codeSpan
|
||||
p.inlineCallback['\n'] = lineBreak
|
||||
p.inlineCallback['['] = link
|
||||
p.inlineCallback['<'] = leftAngle
|
||||
p.inlineCallback['\\'] = escape
|
||||
p.inlineCallback['&'] = entity
|
||||
|
||||
if extensions&EXTENSION_AUTOLINK != 0 {
|
||||
p.inlineCallback[':'] = autoLink
|
||||
}
|
||||
|
||||
if extensions&EXTENSION_FOOTNOTES != 0 {
|
||||
p.notes = make([]*reference, 0)
|
||||
p.notesRecord = make(map[string]struct{})
|
||||
}
|
||||
|
||||
first := firstPass(p, input)
|
||||
second := secondPass(p, first)
|
||||
return second
|
||||
}
|
||||
|
||||
// first pass:
|
||||
// - normalize newlines
|
||||
// - extract references (outside of fenced code blocks)
|
||||
// - expand tabs (outside of fenced code blocks)
|
||||
// - copy everything else
|
||||
func firstPass(p *parser, input []byte) []byte {
|
||||
var out bytes.Buffer
|
||||
tabSize := TAB_SIZE_DEFAULT
|
||||
if p.flags&EXTENSION_TAB_SIZE_EIGHT != 0 {
|
||||
tabSize = TAB_SIZE_EIGHT
|
||||
}
|
||||
beg := 0
|
||||
lastFencedCodeBlockEnd := 0
|
||||
for beg < len(input) {
|
||||
// Find end of this line, then process the line.
|
||||
end := beg
|
||||
for end < len(input) && input[end] != '\n' && input[end] != '\r' {
|
||||
end++
|
||||
}
|
||||
|
||||
if p.flags&EXTENSION_FENCED_CODE != 0 {
|
||||
// track fenced code block boundaries to suppress tab expansion
|
||||
// and reference extraction inside them:
|
||||
if beg >= lastFencedCodeBlockEnd {
|
||||
if i := p.fencedCodeBlock(&out, input[beg:], false); i > 0 {
|
||||
lastFencedCodeBlockEnd = beg + i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add the line body if present
|
||||
if end > beg {
|
||||
if end < lastFencedCodeBlockEnd { // Do not expand tabs while inside fenced code blocks.
|
||||
out.Write(input[beg:end])
|
||||
} else if refEnd := isReference(p, input[beg:], tabSize); refEnd > 0 {
|
||||
beg += refEnd
|
||||
continue
|
||||
} else {
|
||||
expandTabs(&out, input[beg:end], tabSize)
|
||||
}
|
||||
}
|
||||
|
||||
if end < len(input) && input[end] == '\r' {
|
||||
end++
|
||||
}
|
||||
if end < len(input) && input[end] == '\n' {
|
||||
end++
|
||||
}
|
||||
out.WriteByte('\n')
|
||||
|
||||
beg = end
|
||||
}
|
||||
|
||||
// empty input?
|
||||
if out.Len() == 0 {
|
||||
out.WriteByte('\n')
|
||||
}
|
||||
|
||||
return out.Bytes()
|
||||
}
|
||||
|
||||
// second pass: actual rendering
|
||||
func secondPass(p *parser, input []byte) []byte {
|
||||
var output bytes.Buffer
|
||||
|
||||
p.r.DocumentHeader(&output)
|
||||
p.block(&output, input)
|
||||
|
||||
if p.flags&EXTENSION_FOOTNOTES != 0 && len(p.notes) > 0 {
|
||||
p.r.Footnotes(&output, func() bool {
|
||||
flags := LIST_ITEM_BEGINNING_OF_LIST
|
||||
for i := 0; i < len(p.notes); i += 1 {
|
||||
ref := p.notes[i]
|
||||
var buf bytes.Buffer
|
||||
if ref.hasBlock {
|
||||
flags |= LIST_ITEM_CONTAINS_BLOCK
|
||||
p.block(&buf, ref.title)
|
||||
} else {
|
||||
p.inline(&buf, ref.title)
|
||||
}
|
||||
p.r.FootnoteItem(&output, ref.link, buf.Bytes(), flags)
|
||||
flags &^= LIST_ITEM_BEGINNING_OF_LIST | LIST_ITEM_CONTAINS_BLOCK
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
p.r.DocumentFooter(&output)
|
||||
|
||||
if p.nesting != 0 {
|
||||
panic("Nesting level did not end at zero")
|
||||
}
|
||||
|
||||
return output.Bytes()
|
||||
}
|
||||
|
||||
//
|
||||
// Link references
|
||||
//
|
||||
// This section implements support for references that (usually) appear
|
||||
// as footnotes in a document, and can be referenced anywhere in the document.
|
||||
// The basic format is:
|
||||
//
|
||||
// [1]: http://www.google.com/ "Google"
|
||||
// [2]: http://www.github.com/ "Github"
|
||||
//
|
||||
// Anywhere in the document, the reference can be linked by referring to its
|
||||
// label, i.e., 1 and 2 in this example, as in:
|
||||
//
|
||||
// This library is hosted on [Github][2], a git hosting site.
|
||||
//
|
||||
// Actual footnotes as specified in Pandoc and supported by some other Markdown
|
||||
// libraries such as php-markdown are also taken care of. They look like this:
|
||||
//
|
||||
// This sentence needs a bit of further explanation.[^note]
|
||||
//
|
||||
// [^note]: This is the explanation.
|
||||
//
|
||||
// Footnotes should be placed at the end of the document in an ordered list.
|
||||
// Inline footnotes such as:
|
||||
//
|
||||
// Inline footnotes^[Not supported.] also exist.
|
||||
//
|
||||
// are not yet supported.
|
||||
|
||||
// References are parsed and stored in this struct.
|
||||
type reference struct {
|
||||
link []byte
|
||||
title []byte
|
||||
noteId int // 0 if not a footnote ref
|
||||
hasBlock bool
|
||||
text []byte
|
||||
}
|
||||
|
||||
func (r *reference) String() string {
|
||||
return fmt.Sprintf("{link: %q, title: %q, text: %q, noteId: %d, hasBlock: %v}",
|
||||
r.link, r.title, r.text, r.noteId, r.hasBlock)
|
||||
}
|
||||
|
||||
// Check whether or not data starts with a reference link.
|
||||
// If so, it is parsed and stored in the list of references
|
||||
// (in the render struct).
|
||||
// Returns the number of bytes to skip to move past it,
|
||||
// or zero if the first line is not a reference.
|
||||
func isReference(p *parser, data []byte, tabSize int) int {
|
||||
// up to 3 optional leading spaces
|
||||
if len(data) < 4 {
|
||||
return 0
|
||||
}
|
||||
i := 0
|
||||
for i < 3 && data[i] == ' ' {
|
||||
i++
|
||||
}
|
||||
|
||||
noteId := 0
|
||||
|
||||
// id part: anything but a newline between brackets
|
||||
if data[i] != '[' {
|
||||
return 0
|
||||
}
|
||||
i++
|
||||
if p.flags&EXTENSION_FOOTNOTES != 0 {
|
||||
if i < len(data) && data[i] == '^' {
|
||||
// we can set it to anything here because the proper noteIds will
|
||||
// be assigned later during the second pass. It just has to be != 0
|
||||
noteId = 1
|
||||
i++
|
||||
}
|
||||
}
|
||||
idOffset := i
|
||||
for i < len(data) && data[i] != '\n' && data[i] != '\r' && data[i] != ']' {
|
||||
i++
|
||||
}
|
||||
if i >= len(data) || data[i] != ']' {
|
||||
return 0
|
||||
}
|
||||
idEnd := i
|
||||
|
||||
// spacer: colon (space | tab)* newline? (space | tab)*
|
||||
i++
|
||||
if i >= len(data) || data[i] != ':' {
|
||||
return 0
|
||||
}
|
||||
i++
|
||||
for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
|
||||
i++
|
||||
}
|
||||
if i < len(data) && (data[i] == '\n' || data[i] == '\r') {
|
||||
i++
|
||||
if i < len(data) && data[i] == '\n' && data[i-1] == '\r' {
|
||||
i++
|
||||
}
|
||||
}
|
||||
for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
|
||||
i++
|
||||
}
|
||||
if i >= len(data) {
|
||||
return 0
|
||||
}
|
||||
|
||||
var (
|
||||
linkOffset, linkEnd int
|
||||
titleOffset, titleEnd int
|
||||
lineEnd int
|
||||
raw []byte
|
||||
hasBlock bool
|
||||
)
|
||||
|
||||
if p.flags&EXTENSION_FOOTNOTES != 0 && noteId != 0 {
|
||||
linkOffset, linkEnd, raw, hasBlock = scanFootnote(p, data, i, tabSize)
|
||||
lineEnd = linkEnd
|
||||
} else {
|
||||
linkOffset, linkEnd, titleOffset, titleEnd, lineEnd = scanLinkRef(p, data, i)
|
||||
}
|
||||
if lineEnd == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// a valid ref has been found
|
||||
|
||||
ref := &reference{
|
||||
noteId: noteId,
|
||||
hasBlock: hasBlock,
|
||||
}
|
||||
|
||||
if noteId > 0 {
|
||||
// reusing the link field for the id since footnotes don't have links
|
||||
ref.link = data[idOffset:idEnd]
|
||||
// if footnote, it's not really a title, it's the contained text
|
||||
ref.title = raw
|
||||
} else {
|
||||
ref.link = data[linkOffset:linkEnd]
|
||||
ref.title = data[titleOffset:titleEnd]
|
||||
}
|
||||
|
||||
// id matches are case-insensitive
|
||||
id := string(bytes.ToLower(data[idOffset:idEnd]))
|
||||
|
||||
p.refs[id] = ref
|
||||
|
||||
return lineEnd
|
||||
}
|
||||
|
||||
func scanLinkRef(p *parser, data []byte, i int) (linkOffset, linkEnd, titleOffset, titleEnd, lineEnd int) {
|
||||
// link: whitespace-free sequence, optionally between angle brackets
|
||||
if data[i] == '<' {
|
||||
i++
|
||||
}
|
||||
linkOffset = i
|
||||
if i == len(data) {
|
||||
return
|
||||
}
|
||||
for i < len(data) && data[i] != ' ' && data[i] != '\t' && data[i] != '\n' && data[i] != '\r' {
|
||||
i++
|
||||
}
|
||||
linkEnd = i
|
||||
if data[linkOffset] == '<' && data[linkEnd-1] == '>' {
|
||||
linkOffset++
|
||||
linkEnd--
|
||||
}
|
||||
|
||||
// optional spacer: (space | tab)* (newline | '\'' | '"' | '(' )
|
||||
for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
|
||||
i++
|
||||
}
|
||||
if i < len(data) && data[i] != '\n' && data[i] != '\r' && data[i] != '\'' && data[i] != '"' && data[i] != '(' {
|
||||
return
|
||||
}
|
||||
|
||||
// compute end-of-line
|
||||
if i >= len(data) || data[i] == '\r' || data[i] == '\n' {
|
||||
lineEnd = i
|
||||
}
|
||||
if i+1 < len(data) && data[i] == '\r' && data[i+1] == '\n' {
|
||||
lineEnd++
|
||||
}
|
||||
|
||||
// optional (space|tab)* spacer after a newline
|
||||
if lineEnd > 0 {
|
||||
i = lineEnd + 1
|
||||
for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
// optional title: any non-newline sequence enclosed in '"() alone on its line
|
||||
if i+1 < len(data) && (data[i] == '\'' || data[i] == '"' || data[i] == '(') {
|
||||
i++
|
||||
titleOffset = i
|
||||
|
||||
// look for EOL
|
||||
for i < len(data) && data[i] != '\n' && data[i] != '\r' {
|
||||
i++
|
||||
}
|
||||
if i+1 < len(data) && data[i] == '\n' && data[i+1] == '\r' {
|
||||
titleEnd = i + 1
|
||||
} else {
|
||||
titleEnd = i
|
||||
}
|
||||
|
||||
// step back
|
||||
i--
|
||||
for i > titleOffset && (data[i] == ' ' || data[i] == '\t') {
|
||||
i--
|
||||
}
|
||||
if i > titleOffset && (data[i] == '\'' || data[i] == '"' || data[i] == ')') {
|
||||
lineEnd = titleEnd
|
||||
titleEnd = i
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// The first bit of this logic is the same as (*parser).listItem, but the rest
|
||||
// is much simpler. This function simply finds the entire block and shifts it
|
||||
// over by one tab if it is indeed a block (just returns the line if it's not).
|
||||
// blockEnd is the end of the section in the input buffer, and contents is the
|
||||
// extracted text that was shifted over one tab. It will need to be rendered at
|
||||
// the end of the document.
|
||||
func scanFootnote(p *parser, data []byte, i, indentSize int) (blockStart, blockEnd int, contents []byte, hasBlock bool) {
|
||||
if i == 0 || len(data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// skip leading whitespace on first line
|
||||
for i < len(data) && data[i] == ' ' {
|
||||
i++
|
||||
}
|
||||
|
||||
blockStart = i
|
||||
|
||||
// find the end of the line
|
||||
blockEnd = i
|
||||
for i < len(data) && data[i-1] != '\n' {
|
||||
i++
|
||||
}
|
||||
|
||||
// get working buffer
|
||||
var raw bytes.Buffer
|
||||
|
||||
// put the first line into the working buffer
|
||||
raw.Write(data[blockEnd:i])
|
||||
blockEnd = i
|
||||
|
||||
// process the following lines
|
||||
containsBlankLine := false
|
||||
|
||||
gatherLines:
|
||||
for blockEnd < len(data) {
|
||||
i++
|
||||
|
||||
// find the end of this line
|
||||
for i < len(data) && data[i-1] != '\n' {
|
||||
i++
|
||||
}
|
||||
|
||||
// if it is an empty line, guess that it is part of this item
|
||||
// and move on to the next line
|
||||
if p.isEmpty(data[blockEnd:i]) > 0 {
|
||||
containsBlankLine = true
|
||||
blockEnd = i
|
||||
continue
|
||||
}
|
||||
|
||||
n := 0
|
||||
if n = isIndented(data[blockEnd:i], indentSize); n == 0 {
|
||||
// this is the end of the block.
|
||||
// we don't want to include this last line in the index.
|
||||
break gatherLines
|
||||
}
|
||||
|
||||
// if there were blank lines before this one, insert a new one now
|
||||
if containsBlankLine {
|
||||
raw.WriteByte('\n')
|
||||
containsBlankLine = false
|
||||
}
|
||||
|
||||
// get rid of that first tab, write to buffer
|
||||
raw.Write(data[blockEnd+n : i])
|
||||
hasBlock = true
|
||||
|
||||
blockEnd = i
|
||||
}
|
||||
|
||||
if data[blockEnd-1] != '\n' {
|
||||
raw.WriteByte('\n')
|
||||
}
|
||||
|
||||
contents = raw.Bytes()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
// Miscellaneous helper functions
|
||||
//
|
||||
//
|
||||
|
||||
// Test if a character is a punctuation symbol.
|
||||
// Taken from a private function in regexp in the stdlib.
|
||||
func ispunct(c byte) bool {
|
||||
for _, r := range []byte("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") {
|
||||
if c == r {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Test if a character is a whitespace character.
|
||||
func isspace(c byte) bool {
|
||||
return ishorizontalspace(c) || isverticalspace(c)
|
||||
}
|
||||
|
||||
// Test if a character is a horizontal whitespace character.
|
||||
func ishorizontalspace(c byte) bool {
|
||||
return c == ' ' || c == '\t'
|
||||
}
|
||||
|
||||
// Test if a character is a vertical whitespace character.
|
||||
func isverticalspace(c byte) bool {
|
||||
return c == '\n' || c == '\r' || c == '\f' || c == '\v'
|
||||
}
|
||||
|
||||
// Test if a character is letter.
|
||||
func isletter(c byte) bool {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
||||
}
|
||||
|
||||
// Test if a character is a letter or a digit.
|
||||
// TODO: check when this is looking for ASCII alnum and when it should use unicode
|
||||
func isalnum(c byte) bool {
|
||||
return (c >= '0' && c <= '9') || isletter(c)
|
||||
}
|
||||
|
||||
// Replace tab characters with spaces, aligning to the next TAB_SIZE column.
|
||||
// always ends output with a newline
|
||||
func expandTabs(out *bytes.Buffer, line []byte, tabSize int) {
|
||||
// first, check for common cases: no tabs, or only tabs at beginning of line
|
||||
i, prefix := 0, 0
|
||||
slowcase := false
|
||||
for i = 0; i < len(line); i++ {
|
||||
if line[i] == '\t' {
|
||||
if prefix == i {
|
||||
prefix++
|
||||
} else {
|
||||
slowcase = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no need to decode runes if all tabs are at the beginning of the line
|
||||
if !slowcase {
|
||||
for i = 0; i < prefix*tabSize; i++ {
|
||||
out.WriteByte(' ')
|
||||
}
|
||||
out.Write(line[prefix:])
|
||||
return
|
||||
}
|
||||
|
||||
// the slow case: we need to count runes to figure out how
|
||||
// many spaces to insert for each tab
|
||||
column := 0
|
||||
i = 0
|
||||
for i < len(line) {
|
||||
start := i
|
||||
for i < len(line) && line[i] != '\t' {
|
||||
_, size := utf8.DecodeRune(line[i:])
|
||||
i += size
|
||||
column++
|
||||
}
|
||||
|
||||
if i > start {
|
||||
out.Write(line[start:i])
|
||||
}
|
||||
|
||||
if i >= len(line) {
|
||||
break
|
||||
}
|
||||
|
||||
for {
|
||||
out.WriteByte(' ')
|
||||
column++
|
||||
if column%tabSize == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
// Find if a line counts as indented or not.
|
||||
// Returns number of characters the indent is (0 = not indented).
|
||||
func isIndented(data []byte, indentSize int) int {
|
||||
if len(data) == 0 {
|
||||
return 0
|
||||
}
|
||||
if data[0] == '\t' {
|
||||
return 1
|
||||
}
|
||||
if len(data) < indentSize {
|
||||
return 0
|
||||
}
|
||||
for i := 0; i < indentSize; i++ {
|
||||
if data[i] != ' ' {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
return indentSize
|
||||
}
|
||||
|
||||
// Create a url-safe slug for fragments
|
||||
func slugify(in []byte) []byte {
|
||||
if len(in) == 0 {
|
||||
return in
|
||||
}
|
||||
out := make([]byte, 0, len(in))
|
||||
sym := false
|
||||
|
||||
for _, ch := range in {
|
||||
if isalnum(ch) {
|
||||
sym = false
|
||||
out = append(out, ch)
|
||||
} else if sym {
|
||||
continue
|
||||
} else {
|
||||
out = append(out, '-')
|
||||
sym = true
|
||||
}
|
||||
}
|
||||
var a, b int
|
||||
var ch byte
|
||||
for a, ch = range out {
|
||||
if ch != '-' {
|
||||
break
|
||||
}
|
||||
}
|
||||
for b = len(out) - 1; b > 0; b-- {
|
||||
if out[b] != '-' {
|
||||
break
|
||||
}
|
||||
}
|
||||
return out[a : b+1]
|
||||
}
|
||||
|
|
@ -1,430 +0,0 @@
|
|||
//
|
||||
// Blackfriday Markdown Processor
|
||||
// Available at http://github.com/russross/blackfriday
|
||||
//
|
||||
// Copyright © 2011 Russ Ross <russ@russross.com>.
|
||||
// Distributed under the Simplified BSD License.
|
||||
// See README.md for details.
|
||||
//
|
||||
|
||||
//
|
||||
//
|
||||
// SmartyPants rendering
|
||||
//
|
||||
//
|
||||
|
||||
package blackfriday
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
type smartypantsData struct {
|
||||
inSingleQuote bool
|
||||
inDoubleQuote bool
|
||||
}
|
||||
|
||||
func wordBoundary(c byte) bool {
|
||||
return c == 0 || isspace(c) || ispunct(c)
|
||||
}
|
||||
|
||||
func tolower(c byte) byte {
|
||||
if c >= 'A' && c <= 'Z' {
|
||||
return c - 'A' + 'a'
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func isdigit(c byte) bool {
|
||||
return c >= '0' && c <= '9'
|
||||
}
|
||||
|
||||
func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote byte, isOpen *bool, addNBSP bool) bool {
|
||||
// edge of the buffer is likely to be a tag that we don't get to see,
|
||||
// so we treat it like text sometimes
|
||||
|
||||
// enumerate all sixteen possibilities for (previousChar, nextChar)
|
||||
// each can be one of {0, space, punct, other}
|
||||
switch {
|
||||
case previousChar == 0 && nextChar == 0:
|
||||
// context is not any help here, so toggle
|
||||
*isOpen = !*isOpen
|
||||
case isspace(previousChar) && nextChar == 0:
|
||||
// [ "] might be [ "<code>foo...]
|
||||
*isOpen = true
|
||||
case ispunct(previousChar) && nextChar == 0:
|
||||
// [!"] hmm... could be [Run!"] or [("<code>...]
|
||||
*isOpen = false
|
||||
case /* isnormal(previousChar) && */ nextChar == 0:
|
||||
// [a"] is probably a close
|
||||
*isOpen = false
|
||||
case previousChar == 0 && isspace(nextChar):
|
||||
// [" ] might be [...foo</code>" ]
|
||||
*isOpen = false
|
||||
case isspace(previousChar) && isspace(nextChar):
|
||||
// [ " ] context is not any help here, so toggle
|
||||
*isOpen = !*isOpen
|
||||
case ispunct(previousChar) && isspace(nextChar):
|
||||
// [!" ] is probably a close
|
||||
*isOpen = false
|
||||
case /* isnormal(previousChar) && */ isspace(nextChar):
|
||||
// [a" ] this is one of the easy cases
|
||||
*isOpen = false
|
||||
case previousChar == 0 && ispunct(nextChar):
|
||||
// ["!] hmm... could be ["$1.95] or [</code>"!...]
|
||||
*isOpen = false
|
||||
case isspace(previousChar) && ispunct(nextChar):
|
||||
// [ "!] looks more like [ "$1.95]
|
||||
*isOpen = true
|
||||
case ispunct(previousChar) && ispunct(nextChar):
|
||||
// [!"!] context is not any help here, so toggle
|
||||
*isOpen = !*isOpen
|
||||
case /* isnormal(previousChar) && */ ispunct(nextChar):
|
||||
// [a"!] is probably a close
|
||||
*isOpen = false
|
||||
case previousChar == 0 /* && isnormal(nextChar) */ :
|
||||
// ["a] is probably an open
|
||||
*isOpen = true
|
||||
case isspace(previousChar) /* && isnormal(nextChar) */ :
|
||||
// [ "a] this is one of the easy cases
|
||||
*isOpen = true
|
||||
case ispunct(previousChar) /* && isnormal(nextChar) */ :
|
||||
// [!"a] is probably an open
|
||||
*isOpen = true
|
||||
default:
|
||||
// [a'b] maybe a contraction?
|
||||
*isOpen = false
|
||||
}
|
||||
|
||||
// Note that with the limited lookahead, this non-breaking
|
||||
// space will also be appended to single double quotes.
|
||||
if addNBSP && !*isOpen {
|
||||
out.WriteString(" ")
|
||||
}
|
||||
|
||||
out.WriteByte('&')
|
||||
if *isOpen {
|
||||
out.WriteByte('l')
|
||||
} else {
|
||||
out.WriteByte('r')
|
||||
}
|
||||
out.WriteByte(quote)
|
||||
out.WriteString("quo;")
|
||||
|
||||
if addNBSP && *isOpen {
|
||||
out.WriteString(" ")
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func smartSingleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||||
if len(text) >= 2 {
|
||||
t1 := tolower(text[1])
|
||||
|
||||
if t1 == '\'' {
|
||||
nextChar := byte(0)
|
||||
if len(text) >= 3 {
|
||||
nextChar = text[2]
|
||||
}
|
||||
if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote, false) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
if (t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') && (len(text) < 3 || wordBoundary(text[2])) {
|
||||
out.WriteString("’")
|
||||
return 0
|
||||
}
|
||||
|
||||
if len(text) >= 3 {
|
||||
t2 := tolower(text[2])
|
||||
|
||||
if ((t1 == 'r' && t2 == 'e') || (t1 == 'l' && t2 == 'l') || (t1 == 'v' && t2 == 'e')) &&
|
||||
(len(text) < 4 || wordBoundary(text[3])) {
|
||||
out.WriteString("’")
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nextChar := byte(0)
|
||||
if len(text) > 1 {
|
||||
nextChar = text[1]
|
||||
}
|
||||
if smartQuoteHelper(out, previousChar, nextChar, 's', &smrt.inSingleQuote, false) {
|
||||
return 0
|
||||
}
|
||||
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
|
||||
func smartParens(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||||
if len(text) >= 3 {
|
||||
t1 := tolower(text[1])
|
||||
t2 := tolower(text[2])
|
||||
|
||||
if t1 == 'c' && t2 == ')' {
|
||||
out.WriteString("©")
|
||||
return 2
|
||||
}
|
||||
|
||||
if t1 == 'r' && t2 == ')' {
|
||||
out.WriteString("®")
|
||||
return 2
|
||||
}
|
||||
|
||||
if len(text) >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')' {
|
||||
out.WriteString("™")
|
||||
return 3
|
||||
}
|
||||
}
|
||||
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
|
||||
func smartDash(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||||
if len(text) >= 2 {
|
||||
if text[1] == '-' {
|
||||
out.WriteString("—")
|
||||
return 1
|
||||
}
|
||||
|
||||
if wordBoundary(previousChar) && wordBoundary(text[1]) {
|
||||
out.WriteString("–")
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
|
||||
func smartDashLatex(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||||
if len(text) >= 3 && text[1] == '-' && text[2] == '-' {
|
||||
out.WriteString("—")
|
||||
return 2
|
||||
}
|
||||
if len(text) >= 2 && text[1] == '-' {
|
||||
out.WriteString("–")
|
||||
return 1
|
||||
}
|
||||
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
|
||||
func smartAmpVariant(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte, quote byte, addNBSP bool) int {
|
||||
if bytes.HasPrefix(text, []byte(""")) {
|
||||
nextChar := byte(0)
|
||||
if len(text) >= 7 {
|
||||
nextChar = text[6]
|
||||
}
|
||||
if smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote, addNBSP) {
|
||||
return 5
|
||||
}
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(text, []byte("�")) {
|
||||
return 3
|
||||
}
|
||||
|
||||
out.WriteByte('&')
|
||||
return 0
|
||||
}
|
||||
|
||||
func smartAmp(angledQuotes, addNBSP bool) func(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||||
var quote byte = 'd'
|
||||
if angledQuotes {
|
||||
quote = 'a'
|
||||
}
|
||||
|
||||
return func(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||||
return smartAmpVariant(out, smrt, previousChar, text, quote, addNBSP)
|
||||
}
|
||||
}
|
||||
|
||||
func smartPeriod(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||||
if len(text) >= 3 && text[1] == '.' && text[2] == '.' {
|
||||
out.WriteString("…")
|
||||
return 2
|
||||
}
|
||||
|
||||
if len(text) >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.' {
|
||||
out.WriteString("…")
|
||||
return 4
|
||||
}
|
||||
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
|
||||
func smartBacktick(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||||
if len(text) >= 2 && text[1] == '`' {
|
||||
nextChar := byte(0)
|
||||
if len(text) >= 3 {
|
||||
nextChar = text[2]
|
||||
}
|
||||
if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote, false) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
|
||||
func smartNumberGeneric(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||||
if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 {
|
||||
// is it of the form digits/digits(word boundary)?, i.e., \d+/\d+\b
|
||||
// note: check for regular slash (/) or fraction slash (⁄, 0x2044, or 0xe2 81 84 in utf-8)
|
||||
// and avoid changing dates like 1/23/2005 into fractions.
|
||||
numEnd := 0
|
||||
for len(text) > numEnd && isdigit(text[numEnd]) {
|
||||
numEnd++
|
||||
}
|
||||
if numEnd == 0 {
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
denStart := numEnd + 1
|
||||
if len(text) > numEnd+3 && text[numEnd] == 0xe2 && text[numEnd+1] == 0x81 && text[numEnd+2] == 0x84 {
|
||||
denStart = numEnd + 3
|
||||
} else if len(text) < numEnd+2 || text[numEnd] != '/' {
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
denEnd := denStart
|
||||
for len(text) > denEnd && isdigit(text[denEnd]) {
|
||||
denEnd++
|
||||
}
|
||||
if denEnd == denStart {
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
if len(text) == denEnd || wordBoundary(text[denEnd]) && text[denEnd] != '/' {
|
||||
out.WriteString("<sup>")
|
||||
out.Write(text[:numEnd])
|
||||
out.WriteString("</sup>⁄<sub>")
|
||||
out.Write(text[denStart:denEnd])
|
||||
out.WriteString("</sub>")
|
||||
return denEnd - 1
|
||||
}
|
||||
}
|
||||
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
|
||||
func smartNumber(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||||
if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 {
|
||||
if text[0] == '1' && text[1] == '/' && text[2] == '2' {
|
||||
if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' {
|
||||
out.WriteString("½")
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
if text[0] == '1' && text[1] == '/' && text[2] == '4' {
|
||||
if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 5 && tolower(text[3]) == 't' && tolower(text[4]) == 'h') {
|
||||
out.WriteString("¼")
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
if text[0] == '3' && text[1] == '/' && text[2] == '4' {
|
||||
if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 6 && tolower(text[3]) == 't' && tolower(text[4]) == 'h' && tolower(text[5]) == 's') {
|
||||
out.WriteString("¾")
|
||||
return 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
|
||||
func smartDoubleQuoteVariant(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte, quote byte) int {
|
||||
nextChar := byte(0)
|
||||
if len(text) > 1 {
|
||||
nextChar = text[1]
|
||||
}
|
||||
if !smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote, false) {
|
||||
out.WriteString(""")
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func smartDoubleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||||
return smartDoubleQuoteVariant(out, smrt, previousChar, text, 'd')
|
||||
}
|
||||
|
||||
func smartAngledDoubleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||||
return smartDoubleQuoteVariant(out, smrt, previousChar, text, 'a')
|
||||
}
|
||||
|
||||
func smartLeftAngle(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||||
i := 0
|
||||
|
||||
for i < len(text) && text[i] != '>' {
|
||||
i++
|
||||
}
|
||||
|
||||
out.Write(text[:i+1])
|
||||
return i
|
||||
}
|
||||
|
||||
type smartCallback func(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int
|
||||
|
||||
type smartypantsRenderer [256]smartCallback
|
||||
|
||||
var (
|
||||
smartAmpAngled = smartAmp(true, false)
|
||||
smartAmpAngledNBSP = smartAmp(true, true)
|
||||
smartAmpRegular = smartAmp(false, false)
|
||||
smartAmpRegularNBSP = smartAmp(false, true)
|
||||
)
|
||||
|
||||
func smartypants(flags int) *smartypantsRenderer {
|
||||
r := new(smartypantsRenderer)
|
||||
addNBSP := flags&HTML_SMARTYPANTS_QUOTES_NBSP != 0
|
||||
if flags&HTML_SMARTYPANTS_ANGLED_QUOTES == 0 {
|
||||
r['"'] = smartDoubleQuote
|
||||
if !addNBSP {
|
||||
r['&'] = smartAmpRegular
|
||||
} else {
|
||||
r['&'] = smartAmpRegularNBSP
|
||||
}
|
||||
} else {
|
||||
r['"'] = smartAngledDoubleQuote
|
||||
if !addNBSP {
|
||||
r['&'] = smartAmpAngled
|
||||
} else {
|
||||
r['&'] = smartAmpAngledNBSP
|
||||
}
|
||||
}
|
||||
r['\''] = smartSingleQuote
|
||||
r['('] = smartParens
|
||||
if flags&HTML_SMARTYPANTS_DASHES != 0 {
|
||||
if flags&HTML_SMARTYPANTS_LATEX_DASHES == 0 {
|
||||
r['-'] = smartDash
|
||||
} else {
|
||||
r['-'] = smartDashLatex
|
||||
}
|
||||
}
|
||||
r['.'] = smartPeriod
|
||||
if flags&HTML_SMARTYPANTS_FRACTIONS == 0 {
|
||||
r['1'] = smartNumber
|
||||
r['3'] = smartNumber
|
||||
} else {
|
||||
for ch := '1'; ch <= '9'; ch++ {
|
||||
r[ch] = smartNumberGeneric
|
||||
}
|
||||
}
|
||||
r['<'] = smartLeftAngle
|
||||
r['`'] = smartBacktick
|
||||
return r
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# github.com/cpuguy83/go-md2man v1.0.10
|
||||
## explicit
|
||||
github.com/cpuguy83/go-md2man
|
||||
github.com/cpuguy83/go-md2man/md2man
|
||||
# github.com/russross/blackfriday v1.5.2
|
||||
github.com/russross/blackfriday
|
||||
Loading…
Reference in New Issue