Commit b3336f69 authored by Darren Shepherd's avatar Darren Shepherd

Update kine and dynamiclistener

parent e2431bdf
...@@ -31,6 +31,7 @@ replace ( ...@@ -31,6 +31,7 @@ replace (
github.com/prometheus/client_model => github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 github.com/prometheus/client_model => github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910
github.com/prometheus/common => github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 github.com/prometheus/common => github.com/prometheus/common v0.0.0-20181126121408-4724e9255275
github.com/prometheus/procfs => github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a github.com/prometheus/procfs => github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a
github.com/rancher/kine => ../kine
k8s.io/api => github.com/rancher/kubernetes/staging/src/k8s.io/api v1.16.2-k3s.1 k8s.io/api => github.com/rancher/kubernetes/staging/src/k8s.io/api v1.16.2-k3s.1
k8s.io/apiextensions-apiserver => github.com/rancher/kubernetes/staging/src/k8s.io/apiextensions-apiserver v1.16.2-k3s.1 k8s.io/apiextensions-apiserver => github.com/rancher/kubernetes/staging/src/k8s.io/apiextensions-apiserver v1.16.2-k3s.1
k8s.io/apimachinery => github.com/rancher/kubernetes/staging/src/k8s.io/apimachinery v1.16.2-k3s.1 k8s.io/apimachinery => github.com/rancher/kubernetes/staging/src/k8s.io/apimachinery v1.16.2-k3s.1
...@@ -64,6 +65,7 @@ require ( ...@@ -64,6 +65,7 @@ require (
github.com/bhendo/go-powershell v0.0.0-20190719160123-219e7fb4e41e // indirect github.com/bhendo/go-powershell v0.0.0-20190719160123-219e7fb4e41e // indirect
github.com/bronze1man/goStrongswanVici v0.0.0-20190828090544-27d02f80ba40 // indirect github.com/bronze1man/goStrongswanVici v0.0.0-20190828090544-27d02f80ba40 // indirect
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23 // indirect github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23 // indirect
github.com/canonical/go-dqlite v1.1.0
github.com/containerd/cgroups v0.0.0-20190923161937-abd0b19954a6 // indirect github.com/containerd/cgroups v0.0.0-20190923161937-abd0b19954a6 // indirect
github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69 github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69
github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 // indirect github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 // indirect
...@@ -75,30 +77,32 @@ require ( ...@@ -75,30 +77,32 @@ require (
github.com/containernetworking/plugins v0.8.2 // indirect github.com/containernetworking/plugins v0.8.2 // indirect
github.com/coreos/flannel v0.11.0 github.com/coreos/flannel v0.11.0
github.com/coreos/go-iptables v0.4.2 github.com/coreos/go-iptables v0.4.2
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f
github.com/docker/docker v0.7.3-0.20190731001754-589f1dad8dad github.com/docker/docker v0.7.3-0.20190731001754-589f1dad8dad
github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/libnetwork v0.8.0-dev.2.0.20190624125649-f0e46a78ea34 // indirect github.com/docker/libnetwork v0.8.0-dev.2.0.20190624125649-f0e46a78ea34 // indirect
github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4 // indirect
github.com/go-bindata/go-bindata v3.1.2+incompatible github.com/go-bindata/go-bindata v3.1.2+incompatible
github.com/go-sql-driver/mysql v1.4.1 github.com/go-sql-driver/mysql v1.4.1
github.com/gofrs/flock v0.7.1 // indirect github.com/gofrs/flock v0.7.1 // indirect
github.com/gogo/googleapis v1.3.0 // indirect github.com/gogo/googleapis v1.3.0 // indirect
github.com/google/tcpproxy v0.0.0-20180808230851-dfa16c61dad2 github.com/google/tcpproxy v0.0.0-20180808230851-dfa16c61dad2
github.com/gorilla/mux v1.7.3 github.com/gorilla/mux v1.7.3
github.com/gorilla/websocket v1.4.0 github.com/gorilla/websocket v1.4.1
github.com/juju/errors v0.0.0-20190806202954-0232dcc7464d // indirect github.com/juju/errors v0.0.0-20190806202954-0232dcc7464d // indirect
github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2 // indirect github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2 // indirect
github.com/kubernetes-sigs/cri-tools v0.0.0-00010101000000-000000000000 github.com/kubernetes-sigs/cri-tools v0.0.0-00010101000000-000000000000
github.com/lib/pq v1.1.1 github.com/lib/pq v1.1.1
github.com/lxc/lxd v0.0.0-20191108214106-60ea15630455
github.com/mattn/go-sqlite3 v1.10.0 github.com/mattn/go-sqlite3 v1.10.0
github.com/mindprince/gonvml v0.0.0-20190828220739-9ebdce4bb989 // indirect github.com/mindprince/gonvml v0.0.0-20190828220739-9ebdce4bb989 // indirect
github.com/natefinch/lumberjack v2.0.0+incompatible github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/opencontainers/runc v1.0.0-rc2.0.20190611121236-6cc515888830 github.com/opencontainers/runc v1.0.0-rc2.0.20190611121236-6cc515888830
github.com/pkg/errors v0.8.1 github.com/pkg/errors v0.8.1
github.com/rakelkar/gonetsh v0.0.0-20190719023240-501daadcadf8 // indirect github.com/rakelkar/gonetsh v0.0.0-20190719023240-501daadcadf8 // indirect
github.com/rancher/dynamiclistener v0.1.1-0.20191108205817-245f86cc340a github.com/rancher/dynamiclistener v0.1.1-0.20191110035254-aaa5bc0d2a07
github.com/rancher/helm-controller v0.2.2 github.com/rancher/helm-controller v0.2.2
github.com/rancher/kine v0.1.2-0.20191107225357-527576e3452f github.com/rancher/kine v0.2.0
github.com/rancher/remotedialer v0.2.0 github.com/rancher/remotedialer v0.2.0
github.com/rancher/wrangler v0.2.0 github.com/rancher/wrangler v0.2.0
github.com/rancher/wrangler-api v0.2.0 github.com/rancher/wrangler-api v0.2.0
...@@ -108,11 +112,11 @@ require ( ...@@ -108,11 +112,11 @@ require (
github.com/tchap/go-patricia v2.3.0+incompatible // indirect github.com/tchap/go-patricia v2.3.0+incompatible // indirect
github.com/theckman/go-flock v0.7.1 // indirect github.com/theckman/go-flock v0.7.1 // indirect
github.com/urfave/cli v1.21.0 github.com/urfave/cli v1.21.0
go.etcd.io/bbolt v1.3.3 // indirect
golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3
google.golang.org/grpc v1.23.0 google.golang.org/grpc v1.23.0
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect
gopkg.in/robfig/cron.v2 v2.0.0-20150107220207-be2e0b0deed5 // indirect
gopkg.in/yaml.v2 v2.2.4 gopkg.in/yaml.v2 v2.2.4
k8s.io/api v0.0.0 k8s.io/api v0.0.0
k8s.io/apimachinery v0.0.0 k8s.io/apimachinery v0.0.0
......
language: go
go:
- 1.6
- tip
sudo: false
install:
# Get all imported packages
- make install-deps install-deps-dev
# Basic build errors
- make build
script:
# Lint
- make format-lint
- make import-lint
- make copyright-lint
# Run tests
- make test
matrix:
allow_failures:
- go: tip
fast_finish: true
Copyright (C) 2016 Trevor N. Suarez (Rican7)
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.
# Define some VCS context
PARENT_BRANCH ?= master
# Set a default `min_confidence` value for `golint`
GOLINT_MIN_CONFIDENCE ?= 0.3
# Set flags for `gofmt`
GOFMT_FLAGS ?= -s
all: install-deps build install
clean:
go clean -i -x ./...
build:
go build -v ./...
install:
go install ./...
install-deps:
go get -d -t ./...
install-deps-dev: install-deps
go get github.com/golang/lint/golint
go get golang.org/x/tools/cmd/goimports
update-deps:
go get -d -t -u ./...
update-deps-dev: update-deps
go get -u github.com/golang/lint/golint
go get -u golang.org/x/tools/cmd/goimports
test:
go test -v ./...
test-with-coverage:
go test -cover ./...
test-with-coverage-formatted:
go test -cover ./... | column -t | sort -r
format-lint:
errors=$$(gofmt -l ${GOFMT_FLAGS} .); if [ "$${errors}" != "" ]; then echo "$${errors}"; exit 1; fi
import-lint:
errors=$$(goimports -l .); if [ "$${errors}" != "" ]; then echo "$${errors}"; exit 1; fi
style-lint:
errors=$$(golint -min_confidence=${GOLINT_MIN_CONFIDENCE} ./...); if [ "$${errors}" != "" ]; then echo "$${errors}"; exit 1; fi
copyright-lint:
@old_dates=$$(git diff --diff-filter=ACMRTUXB --name-only "${PARENT_BRANCH}" | xargs grep -E '[Cc]opyright(\s+)[©Cc]?(\s+)[0-9]{4}' | grep -E -v "[Cc]opyright(\s+)[©Cc]?(\s+)$$(date '+%Y')"); if [ "$${old_dates}" != "" ]; then printf "The following files contain outdated copyrights:\n$${old_dates}\n\nThis can be fixed with 'make copyright-fix'\n"; exit 1; fi
lint: install-deps-dev format-lint import-lint style-lint copyright-lint
format-fix:
gofmt -w ${GOFMT_FLAGS} .
import-fix:
goimports -w .
copyright-fix:
@git diff --diff-filter=ACMRTUXB --name-only "${PARENT_BRANCH}" | xargs -I '_FILENAME' -- sh -c 'sed -i.bak "s/\([Cc]opyright\([[:space:]][©Cc]\{0,1\}[[:space:]]*\)\)[0-9]\{4\}/\1"$$(date '+%Y')"/g" _FILENAME && rm _FILENAME.bak'
vet:
go vet ./...
.PHONY: all clean build install install-deps install-deps-dev update-deps update-deps-dev test test-with-coverage test-with-coverage-formatted format-lint import-lint style-lint copyright-lint lint format-fix import-fix copyright-fix vet
# retry
[![Build Status](https://travis-ci.org/Rican7/retry.svg?branch=master)](https://travis-ci.org/Rican7/retry)
[![GoDoc](https://godoc.org/github.com/Rican7/retry?status.png)](https://godoc.org/github.com/Rican7/retry)
[![Go Report Card](https://goreportcard.com/badge/Rican7/retry)](http://goreportcard.com/report/Rican7/retry)
[![Latest Stable Version](https://img.shields.io/github/release/Rican7/retry.svg?style=flat)](https://github.com/Rican7/retry/releases)
A simple, stateless, functional mechanism to perform actions repetitively until successful.
## Project Status
This project is currently in "pre-release". While the code is heavily tested, the API may change.
Vendor (commit or lock) this dependency if you plan on using it.
## Install
`go get github.com/Rican7/retry`
## Examples
### Basic
```go
Retry(func(attempt uint) error {
return nil // Do something that may or may not cause an error
})
```
### File Open
```go
const logFilePath = "/var/log/myapp.log"
var logFile *os.File
err := Retry(func(attempt uint) error {
var err error
logFile, err = os.Open(logFilePath)
return err
})
if nil != err {
log.Fatalf("Unable to open file %q with error %q", logFilePath, err)
}
```
### HTTP request with strategies and backoff
```go
var response *http.Response
action := func(attempt uint) error {
var err error
response, err = http.Get("https://api.github.com/repos/Rican7/retry")
if nil == err && nil != response && response.StatusCode > 200 {
err = fmt.Errorf("failed to fetch (attempt #%d) with status code: %d", attempt, response.StatusCode)
}
return err
}
err := Retry(
action,
strategy.Limit(5),
strategy.Backoff(backoff.Fibonacci(10*time.Millisecond)),
)
if nil != err {
log.Fatalf("Failed to fetch repository with error %q", err)
}
```
### Retry with backoff jitter
```go
action := func(attempt uint) error {
return errors.New("something happened")
}
seed := time.Now().UnixNano()
random := rand.New(rand.NewSource(seed))
Retry(
action,
strategy.Limit(5),
strategy.BackoffWithJitter(
backoff.BinaryExponential(10*time.Millisecond),
jitter.Deviation(random, 0.5),
),
)
```
// Package backoff provides stateless methods of calculating durations based on
// a number of attempts made.
//
// Copyright © 2016 Trevor N. Suarez (Rican7)
package backoff
import (
"math"
"time"
)
// Algorithm defines a function that calculates a time.Duration based on
// the given retry attempt number.
type Algorithm func(attempt uint) time.Duration
// Incremental creates a Algorithm that increments the initial duration
// by the given increment for each attempt.
func Incremental(initial, increment time.Duration) Algorithm {
return func(attempt uint) time.Duration {
return initial + (increment * time.Duration(attempt))
}
}
// Linear creates a Algorithm that linearly multiplies the factor
// duration by the attempt number for each attempt.
func Linear(factor time.Duration) Algorithm {
return func(attempt uint) time.Duration {
return (factor * time.Duration(attempt))
}
}
// Exponential creates a Algorithm that multiplies the factor duration by
// an exponentially increasing factor for each attempt, where the factor is
// calculated as the given base raised to the attempt number.
func Exponential(factor time.Duration, base float64) Algorithm {
return func(attempt uint) time.Duration {
return (factor * time.Duration(math.Pow(base, float64(attempt))))
}
}
// BinaryExponential creates a Algorithm that multiplies the factor
// duration by an exponentially increasing factor for each attempt, where the
// factor is calculated as `2` raised to the attempt number (2^attempt).
func BinaryExponential(factor time.Duration) Algorithm {
return Exponential(factor, 2)
}
// Fibonacci creates a Algorithm that multiplies the factor duration by
// an increasing factor for each attempt, where the factor is the Nth number in
// the Fibonacci sequence.
func Fibonacci(factor time.Duration) Algorithm {
return func(attempt uint) time.Duration {
return (factor * time.Duration(fibonacciNumber(attempt)))
}
}
// fibonacciNumber calculates the Fibonacci sequence number for the given
// sequence position.
func fibonacciNumber(n uint) uint {
if 0 == n {
return 0
} else if 1 == n {
return 1
} else {
return fibonacciNumber(n-1) + fibonacciNumber(n-2)
}
}
// Package jitter provides methods of transforming durations.
//
// Copyright © 2016 Trevor N. Suarez (Rican7)
package jitter
import (
"math"
"math/rand"
"time"
)
// Transformation defines a function that calculates a time.Duration based on
// the given duration.
type Transformation func(duration time.Duration) time.Duration
// Full creates a Transformation that transforms a duration into a result
// duration in [0, n) randomly, where n is the given duration.
//
// The given generator is what is used to determine the random transformation.
// If a nil generator is passed, a default one will be provided.
//
// Inspired by https://www.awsarchitectureblog.com/2015/03/backoff.html
func Full(generator *rand.Rand) Transformation {
random := fallbackNewRandom(generator)
return func(duration time.Duration) time.Duration {
return time.Duration(random.Int63n(int64(duration)))
}
}
// Equal creates a Transformation that transforms a duration into a result
// duration in [n/2, n) randomly, where n is the given duration.
//
// The given generator is what is used to determine the random transformation.
// If a nil generator is passed, a default one will be provided.
//
// Inspired by https://www.awsarchitectureblog.com/2015/03/backoff.html
func Equal(generator *rand.Rand) Transformation {
random := fallbackNewRandom(generator)
return func(duration time.Duration) time.Duration {
return (duration / 2) + time.Duration(random.Int63n(int64(duration))/2)
}
}
// Deviation creates a Transformation that transforms a duration into a result
// duration that deviates from the input randomly by a given factor.
//
// The given generator is what is used to determine the random transformation.
// If a nil generator is passed, a default one will be provided.
//
// Inspired by https://developers.google.com/api-client-library/java/google-http-java-client/backoff
func Deviation(generator *rand.Rand, factor float64) Transformation {
random := fallbackNewRandom(generator)
return func(duration time.Duration) time.Duration {
min := int64(math.Floor(float64(duration) * (1 - factor)))
max := int64(math.Ceil(float64(duration) * (1 + factor)))
return time.Duration(random.Int63n(max-min) + min)
}
}
// NormalDistribution creates a Transformation that transforms a duration into a
// result duration based on a normal distribution of the input and the given
// standard deviation.
//
// The given generator is what is used to determine the random transformation.
// If a nil generator is passed, a default one will be provided.
func NormalDistribution(generator *rand.Rand, standardDeviation float64) Transformation {
random := fallbackNewRandom(generator)
return func(duration time.Duration) time.Duration {
return time.Duration(random.NormFloat64()*standardDeviation + float64(duration))
}
}
// fallbackNewRandom returns the passed in random instance if it's not nil,
// and otherwise returns a new random instance seeded with the current time.
func fallbackNewRandom(random *rand.Rand) *rand.Rand {
// Return the passed in value if it's already not null
if nil != random {
return random
}
seed := time.Now().UnixNano()
return rand.New(rand.NewSource(seed))
}
// Package retry provides a simple, stateless, functional mechanism to perform
// actions repetitively until successful.
//
// Copyright © 2016 Trevor N. Suarez (Rican7)
package retry
import "github.com/Rican7/retry/strategy"
// Action defines a callable function that package retry can handle.
type Action func(attempt uint) error
// Retry takes an action and performs it, repetitively, until successful.
//
// Optionally, strategies may be passed that assess whether or not an attempt
// should be made.
func Retry(action Action, strategies ...strategy.Strategy) error {
var err error
for attempt := uint(0); (0 == attempt || nil != err) && shouldAttempt(attempt, strategies...); attempt++ {
err = action(attempt)
}
return err
}
// shouldAttempt evaluates the provided strategies with the given attempt to
// determine if the Retry loop should make another attempt.
func shouldAttempt(attempt uint, strategies ...strategy.Strategy) bool {
shouldAttempt := true
for i := 0; shouldAttempt && i < len(strategies); i++ {
shouldAttempt = shouldAttempt && strategies[i](attempt)
}
return shouldAttempt
}
// Package strategy provides a way to change the way that retry is performed.
//
// Copyright © 2016 Trevor N. Suarez (Rican7)
package strategy
import (
"time"
"github.com/Rican7/retry/backoff"
"github.com/Rican7/retry/jitter"
)
// Strategy defines a function that Retry calls before every successive attempt
// to determine whether it should make the next attempt or not. Returning `true`
// allows for the next attempt to be made. Returning `false` halts the retrying
// process and returns the last error returned by the called Action.
//
// The strategy will be passed an "attempt" number on each successive retry
// iteration, starting with a `0` value before the first attempt is actually
// made. This allows for a pre-action delay, etc.
type Strategy func(attempt uint) bool
// Limit creates a Strategy that limits the number of attempts that Retry will
// make.
func Limit(attemptLimit uint) Strategy {
return func(attempt uint) bool {
return (attempt <= attemptLimit)
}
}
// Delay creates a Strategy that waits the given duration before the first
// attempt is made.
func Delay(duration time.Duration) Strategy {
return func(attempt uint) bool {
if 0 == attempt {
time.Sleep(duration)
}
return true
}
}
// Wait creates a Strategy that waits the given durations for each attempt after
// the first. If the number of attempts is greater than the number of durations
// provided, then the strategy uses the last duration provided.
func Wait(durations ...time.Duration) Strategy {
return func(attempt uint) bool {
if 0 < attempt && 0 < len(durations) {
durationIndex := int(attempt - 1)
if len(durations) <= durationIndex {
durationIndex = len(durations) - 1
}
time.Sleep(durations[durationIndex])
}
return true
}
}
// Backoff creates a Strategy that waits before each attempt, with a duration as
// defined by the given backoff.Algorithm.
func Backoff(algorithm backoff.Algorithm) Strategy {
return BackoffWithJitter(algorithm, noJitter())
}
// BackoffWithJitter creates a Strategy that waits before each attempt, with a
// duration as defined by the given backoff.Algorithm and jitter.Transformation.
func BackoffWithJitter(algorithm backoff.Algorithm, transformation jitter.Transformation) Strategy {
return func(attempt uint) bool {
if 0 < attempt {
time.Sleep(transformation(algorithm(attempt)))
}
return true
}
}
// noJitter creates a jitter.Transformation that simply returns the input.
func noJitter() jitter.Transformation {
return func(duration time.Duration) time.Duration {
return duration
}
}
;;; Directory Local Variables
;;; For more information see (info "(emacs) Directory Variables")
((go-mode
. ((go-test-args . "-tags libsqlite3 -timeout 10s")
(eval
. (set
(make-local-variable 'flycheck-go-build-tags)
'("libsqlite3"))))))
.sqlite
demo
profile.coverprofile
overalls.coverprofile
dist: xenial
language: go
addons:
apt:
sources:
- sourceline: 'ppa:dqlite/master'
packages:
- golint
- libsqlite3-dev
- libuv1-dev
- libraft-dev
- libco-dev
- libdqlite-dev
before_install:
- go get github.com/go-playground/overalls
- go get github.com/mattn/goveralls
- go get github.com/tsenart/deadcode
script:
- go get -t -tags libsqlite3 ./...
- go vet -tags libsqlite3 ./...
- golint
- deadcode
- project=github.com/canonical/go-dqlite
- $GOPATH/bin/overalls -project $project -covermode=count -- -tags libsqlite3 -timeout 240s
- $GOPATH/bin/goveralls -coverprofile overalls.coverprofile -service=travis-ci
go:
- "1.12"
Free Ekanayaka <free.ekanayaka@canonical.com>
go-dqlite [![Build Status](https://travis-ci.org/canonical/go-dqlite.png)](https://travis-ci.org/canonical/go-dqlite) [![Coverage Status](https://coveralls.io/repos/github/canonical/go-dqlite/badge.svg?branch=master)](https://coveralls.io/github/canonical/go-dqlite?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/canonical/go-dqlite)](https://goreportcard.com/report/github.com/canonical/go-dqlite) [![GoDoc](https://godoc.org/github.com/canonical/go-dqlite?status.svg)](https://godoc.org/github.com/canonical/go-dqlite)
======
This repository provides the `go-dqlite` Go package, containing bindings for the
[dqlite](https://github.com/canonical/dqlite) C library and a pure-Go
client for the dqlite wire [protocol](https://github.com/canonical/dqlite/blob/master/doc/protocol.md).
Usage
-----
The best way to understand how to use the ```go-dqlite``` package is probably by
looking at the source code of the [demo
program](https://github.com/canonical/go-dqlite/tree/master/cmd/dqlite-demo) and
use it as example.
Build
-----
In order to use the go-dqlite package in your application, you'll need to have
the [dqlite](https://github.com/canonical/dqlite) C library installed on your
system, along with its dependencies. You then need to pass the ```-tags```
argument to the Go tools when building or testing your packages, for example:
```bash
go build -tags libsqlite3
go test -tags libsqlite3
```
Documentation
-------------
The documentation for this package can be found on [Godoc](http://godoc.org/github.com/canonical/go-dqlite).
Demo
----
To see dqlite in action, either install the Debian package from the PPA:
```bash
sudo add-apt-repository -y ppa:dqlite/v1
sudo apt install dqlite
```
or build the dqlite C library and its dependencies from source, as described
[here](https://github.com/canonical/dqlite#build), and then run:
```
go install -tags libsqlite3 ./cmd/dqlite-demo
```
from the top-level directory of this repository.
Once the ```dqlite-demo``` binary is installed, start three nodes of the demo
application, respectively with IDs ```1```, ```2,``` and ```3```:
```bash
dqlite-demo start 1 &
dqlite-demo start 2 &
dqlite-demo start 3 &
```
The node with ID ```1``` automatically becomes the leader of a single node
cluster, while the nodes with IDs ```2``` and ```3``` are waiting to be notified
what cluster they belong to. Let's make nodes ```2``` and ```3``` join the
cluster:
```bash
dqlite-demo add 2
dqlite-demo add 3
```
Now we can start using the cluster. The demo application is just a simple
key/value store that stores data in a SQLite table. Let's insert a key pair:
```bash
dqlite-demo update my-key my-value
```
and then retrive it from the database:
```bash
dqlite-demo query my-key
```
Currently node ```1``` is the leader. If we stop it and then try to query the
key again we'll notice that the ```query``` command hangs for a bit waiting for
the failover to occur and for another node to step up as leader:
```
kill -TERM %1; sleep 0.1; dqlite-demo query my-key
```
package client
import (
"context"
"encoding/binary"
"io"
"net"
"strings"
"github.com/canonical/go-dqlite/internal/protocol"
"github.com/pkg/errors"
)
// DialFunc is a function that can be used to establish a network connection.
type DialFunc = protocol.DialFunc
// Client speaks the dqlite wire protocol.
type Client struct {
protocol *protocol.Protocol
}
// Option that can be used to tweak client parameters.
type Option func(*options)
type options struct {
DialFunc DialFunc
LogFunc LogFunc
}
// WithDialFunc sets a custom dial function for creating the client network
// connection.
func WithDialFunc(dial DialFunc) Option {
return func(options *options) {
options.DialFunc = dial
}
}
// WithLogFunc sets a custom log function.
// connection.
func WithLogFunc(log LogFunc) Option {
return func(options *options) {
options.LogFunc = log
}
}
// New creates a new client connected to the dqlite node with the given
// address.
func New(ctx context.Context, address string, options ...Option) (*Client, error) {
o := defaultOptions()
for _, option := range options {
option(o)
}
// Establish the connection.
conn, err := o.DialFunc(ctx, address)
if err != nil {
return nil, errors.Wrap(err, "failed to establish network connection")
}
// Latest protocol version.
proto := make([]byte, 8)
binary.LittleEndian.PutUint64(proto, protocol.VersionOne)
// Perform the protocol handshake.
n, err := conn.Write(proto)
if err != nil {
conn.Close()
return nil, errors.Wrap(err, "failed to send handshake")
}
if n != 8 {
conn.Close()
return nil, errors.Wrap(io.ErrShortWrite, "failed to send handshake")
}
client := &Client{protocol: protocol.NewProtocol(protocol.VersionOne, conn)}
return client, nil
}
// Leader returns information about the current leader, if any.
func (c *Client) Leader(ctx context.Context) (*NodeInfo, error) {
request := protocol.Message{}
request.Init(16)
response := protocol.Message{}
response.Init(512)
protocol.EncodeLeader(&request)
if err := c.protocol.Call(ctx, &request, &response); err != nil {
return nil, errors.Wrap(err, "failed to send Leader request")
}
id, address, err := protocol.DecodeNode(&response)
if err != nil {
return nil, errors.Wrap(err, "failed to parse Node response")
}
info := &NodeInfo{ID: id, Address: address}
return info, nil
}
// Cluster returns information about all nodes in the cluster.
func (c *Client) Cluster(ctx context.Context) ([]NodeInfo, error) {
request := protocol.Message{}
request.Init(16)
response := protocol.Message{}
response.Init(512)
protocol.EncodeCluster(&request)
if err := c.protocol.Call(ctx, &request, &response); err != nil {
return nil, errors.Wrap(err, "failed to send Cluster request")
}
servers, err := protocol.DecodeNodes(&response)
if err != nil {
return nil, errors.Wrap(err, "failed to parse Node response")
}
return servers, nil
}
// File holds the content of a single database file.
type File struct {
Name string
Data []byte
}
// Dump the content of the database with the given name. Two files will be
// returned, the first is the main database file (which has the same name as
// the database), the second is the WAL file (which has the same name as the
// database plus the suffix "-wal").
func (c *Client) Dump(ctx context.Context, dbname string) ([]File, error) {
request := protocol.Message{}
request.Init(16)
response := protocol.Message{}
response.Init(512)
protocol.EncodeDump(&request, dbname)
if err := c.protocol.Call(ctx, &request, &response); err != nil {
return nil, errors.Wrap(err, "failed to send dump request")
}
files, err := protocol.DecodeFiles(&response)
if err != nil {
return nil, errors.Wrap(err, "failed to parse files response")
}
defer files.Close()
dump := make([]File, 0)
for {
name, data := files.Next()
if name == "" {
break
}
dump = append(dump, File{Name: name, Data: data})
}
return dump, nil
}
// Add a node to a cluster.
func (c *Client) Add(ctx context.Context, node NodeInfo) error {
request := protocol.Message{}
request.Init(4096)
response := protocol.Message{}
response.Init(4096)
protocol.EncodeJoin(&request, node.ID, node.Address)
if err := c.protocol.Call(ctx, &request, &response); err != nil {
return err
}
protocol.EncodePromote(&request, node.ID)
if err := c.protocol.Call(ctx, &request, &response); err != nil {
return err
}
return nil
}
// Remove a node from the cluster.
func (c *Client) Remove(ctx context.Context, id uint64) error {
request := protocol.Message{}
request.Init(4096)
response := protocol.Message{}
response.Init(4096)
protocol.EncodeRemove(&request, id)
if err := c.protocol.Call(ctx, &request, &response); err != nil {
return err
}
return nil
}
// Close the client.
func (c *Client) Close() error {
return c.protocol.Close()
}
// Create a client options object with sane defaults.
func defaultOptions() *options {
return &options{
DialFunc: DefaultDialFunc,
LogFunc: DefaultLogFunc,
}
}
func DefaultDialFunc(ctx context.Context, address string) (net.Conn, error) {
if strings.HasPrefix(address, "@") {
return protocol.UnixDial(ctx, address)
}
return protocol.TCPDial(ctx, address)
}
package client
import (
"context"
"time"
"github.com/Rican7/retry/backoff"
"github.com/Rican7/retry/strategy"
"github.com/canonical/go-dqlite/internal/protocol"
)
// FindLeader returns a Client connected to the current cluster leader, if any.
func FindLeader(ctx context.Context, store NodeStore, options ...Option) (*Client, error) {
o := defaultOptions()
for _, option := range options {
option(o)
}
config := protocol.Config{
Dial: o.DialFunc,
AttemptTimeout: time.Second,
RetryStrategies: []strategy.Strategy{
strategy.Backoff(backoff.BinaryExponential(time.Millisecond))},
}
connector := protocol.NewConnector(0, store, config, o.LogFunc)
protocol, err := connector.Connect(ctx)
if err != nil {
return nil, err
}
client := &Client{protocol: protocol}
return client, nil
}
package client
import (
"fmt"
"log"
"os"
"github.com/canonical/go-dqlite/internal/logging"
)
// LogFunc is a function that can be used for logging.
type LogFunc = logging.Func
// LogLevel defines the logging level.
type LogLevel = logging.Level
// Available logging levels.
const (
LogDebug = logging.Debug
LogInfo = logging.Info
LogWarn = logging.Warn
LogError = logging.Error
)
// DefaultLogFunc emits messages using the stdlib's logger.
func DefaultLogFunc(l LogLevel, format string, a ...interface{}) {
logger := log.New(os.Stdout, "", log.LstdFlags|log.Lmicroseconds)
format = fmt.Sprintf("[%s]: %s", l.String(), format)
logger.Printf(format, a...)
}
package client
import (
"context"
"database/sql"
"fmt"
"github.com/pkg/errors"
"github.com/canonical/go-dqlite/internal/protocol"
_ "github.com/mattn/go-sqlite3" // Go SQLite bindings
)
// NodeStore is used by a dqlite client to get an initial list of candidate
// dqlite nodes that it can dial in order to find a leader dqlite node to use.
type NodeStore = protocol.NodeStore
// NodeInfo holds information about a single server.
type NodeInfo = protocol.NodeInfo
// InmemNodeStore keeps the list of target dqlite nodes in memory.
type InmemNodeStore = protocol.InmemNodeStore
// NewInmemNodeStore creates NodeStore which stores its data in-memory.
var NewInmemNodeStore = protocol.NewInmemNodeStore
// DatabaseNodeStore persists a list addresses of dqlite nodes in a SQL table.
type DatabaseNodeStore struct {
db *sql.DB // Database handle to use.
schema string // Name of the schema holding the servers table.
table string // Name of the servers table.
column string // Column name in the servers table holding the server address.
}
// DefaultNodeStore creates a new NodeStore using the given filename to
// open a SQLite database, with default names for the schema, table and column
// parameters.
//
// It also creates the table if it doesn't exist yet.
func DefaultNodeStore(filename string) (*DatabaseNodeStore, error) {
// Open the database.
db, err := sql.Open("sqlite3", filename)
if err != nil {
return nil, errors.Wrap(err, "failed to open database")
}
// Since we're setting SQLite single-thread mode, we need to have one
// connection at most.
db.SetMaxOpenConns(1)
// Create the servers table if it does not exist yet.
_, err = db.Exec("CREATE TABLE IF NOT EXISTS servers (address TEXT, UNIQUE(address))")
if err != nil {
return nil, errors.Wrap(err, "failed to create servers table")
}
store := NewNodeStore(db, "main", "servers", "address")
return store, nil
}
// NewNodeStore creates a new NodeStore.
func NewNodeStore(db *sql.DB, schema, table, column string) *DatabaseNodeStore {
return &DatabaseNodeStore{
db: db,
schema: schema,
table: table,
column: column,
}
}
// Get the current servers.
func (d *DatabaseNodeStore) Get(ctx context.Context) ([]NodeInfo, error) {
tx, err := d.db.Begin()
if err != nil {
return nil, errors.Wrap(err, "failed to begin transaction")
}
defer tx.Rollback()
query := fmt.Sprintf("SELECT %s FROM %s.%s", d.column, d.schema, d.table)
rows, err := tx.QueryContext(ctx, query)
if err != nil {
return nil, errors.Wrap(err, "failed to query servers table")
}
defer rows.Close()
servers := make([]NodeInfo, 0)
for rows.Next() {
var address string
err := rows.Scan(&address)
if err != nil {
return nil, errors.Wrap(err, "failed to fetch server address")
}
servers = append(servers, NodeInfo{ID: 1, Address: address})
}
if err := rows.Err(); err != nil {
return nil, errors.Wrap(err, "result set failure")
}
return servers, nil
}
// Set the servers addresses.
func (d *DatabaseNodeStore) Set(ctx context.Context, servers []NodeInfo) error {
tx, err := d.db.Begin()
if err != nil {
return errors.Wrap(err, "failed to begin transaction")
}
query := fmt.Sprintf("DELETE FROM %s.%s", d.schema, d.table)
if _, err := tx.ExecContext(ctx, query); err != nil {
tx.Rollback()
return errors.Wrap(err, "failed to delete existing servers rows")
}
query = fmt.Sprintf("INSERT INTO %s.%s(%s) VALUES (?)", d.schema, d.table, d.column)
stmt, err := tx.PrepareContext(ctx, query)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "failed to prepare insert statement")
}
defer stmt.Close()
for _, server := range servers {
if _, err := stmt.ExecContext(ctx, server.Address); err != nil {
tx.Rollback()
return errors.Wrapf(err, "failed to insert server %s", server.Address)
}
}
if err := tx.Commit(); err != nil {
return errors.Wrap(err, "failed to commit transaction")
}
return nil
}
package bindings
/*
#cgo linux LDFLAGS: -lsqlite3 -lraft -lco -ldqlite
*/
import "C"
package bindings
/*
#include <sqlite3.h>
*/
import "C"
// Error holds information about a SQLite error.
type Error struct {
Code int
Message string
}
func (e Error) Error() string {
if e.Message != "" {
return e.Message
}
return C.GoString(C.sqlite3_errstr(C.int(e.Code)))
}
package bindings
/*
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <assert.h>
#include <stdint.h>
#include <signal.h>
#include <dqlite.h>
#include <raft.h>
#include <sqlite3.h>
#define EMIT_BUF_LEN 1024
typedef unsigned long long nanoseconds_t;
// Duplicate a file descriptor and prevent it from being cloned into child processes.
static int dupCloexec(int oldfd) {
int newfd = -1;
newfd = dup(oldfd);
if (newfd < 0) {
return -1;
}
if (fcntl(newfd, F_SETFD, FD_CLOEXEC) < 0) {
return -1;
}
return newfd;
}
// C to Go trampoline for custom connect function.
int connectWithDial(uintptr_t handle, char *address, int *fd);
// Wrapper to call the Go trampoline.
static int connectTrampoline(void *data, const char *address, int *fd) {
uintptr_t handle = (uintptr_t)(data);
return connectWithDial(handle, (char*)address, fd);
}
// Configure a custom connect function.
static int configConnectFunc(dqlite_node *t, uintptr_t handle) {
return dqlite_node_set_connect_func(t, connectTrampoline, (void*)handle);
}
static int initializeSQLite()
{
int rc;
// Configure SQLite for single-thread mode. This is a global config.
rc = sqlite3_config(SQLITE_CONFIG_SINGLETHREAD);
if (rc != SQLITE_OK) {
assert(rc == SQLITE_MISUSE);
return DQLITE_MISUSE;
}
return 0;
}
static dqlite_node_info *makeInfos(int n) {
return calloc(n, sizeof(dqlite_node_info));
}
static void setInfo(dqlite_node_info *infos, unsigned i, unsigned id, const char *address) {
dqlite_node_info *info = &infos[i];
info->id = id;
info->address = address;
}
*/
import "C"
import (
"context"
"fmt"
"net"
"os"
"sync"
"time"
"unsafe"
"github.com/canonical/go-dqlite/internal/protocol"
)
type Node C.dqlite_node
// Init initializes dqlite global state.
func Init() error {
// FIXME: ignore SIGPIPE, see https://github.com/joyent/libuv/issues/1254
C.signal(C.SIGPIPE, C.SIG_IGN)
// Don't enable single thread mode when running tests. TODO: find a
// better way to expose this functionality.
if os.Getenv("GO_DQLITE_MULTITHREAD") == "1" {
return nil
}
if rc := C.initializeSQLite(); rc != 0 {
return fmt.Errorf("%d", rc)
}
return nil
}
// NewNode creates a new Node instance.
func NewNode(id uint64, address string, dir string) (*Node, error) {
var server *C.dqlite_node
cid := C.unsigned(id)
caddress := C.CString(address)
defer C.free(unsafe.Pointer(caddress))
cdir := C.CString(dir)
defer C.free(unsafe.Pointer(cdir))
if rc := C.dqlite_node_create(cid, caddress, cdir, &server); rc != 0 {
return nil, fmt.Errorf("failed to create task object")
}
return (*Node)(unsafe.Pointer(server)), nil
}
func (s *Node) SetDialFunc(dial protocol.DialFunc) error {
server := (*C.dqlite_node)(unsafe.Pointer(s))
connectLock.Lock()
defer connectLock.Unlock()
connectIndex++
connectRegistry[connectIndex] = dial
if rc := C.configConnectFunc(server, connectIndex); rc != 0 {
return fmt.Errorf("failed to set connect func")
}
return nil
}
func (s *Node) SetBindAddress(address string) error {
server := (*C.dqlite_node)(unsafe.Pointer(s))
caddress := C.CString(address)
defer C.free(unsafe.Pointer(caddress))
if rc := C.dqlite_node_set_bind_address(server, caddress); rc != 0 {
return fmt.Errorf("failed to set bind address")
}
return nil
}
func (s *Node) SetNetworkLatency(nanoseconds uint64) error {
server := (*C.dqlite_node)(unsafe.Pointer(s))
cnanoseconds := C.nanoseconds_t(nanoseconds)
if rc := C.dqlite_node_set_network_latency(server, cnanoseconds); rc != 0 {
return fmt.Errorf("failed to set network latency")
}
return nil
}
func (s *Node) GetBindAddress() string {
server := (*C.dqlite_node)(unsafe.Pointer(s))
return C.GoString(C.dqlite_node_get_bind_address(server))
}
func (s *Node) Start() error {
server := (*C.dqlite_node)(unsafe.Pointer(s))
if rc := C.dqlite_node_start(server); rc != 0 {
return fmt.Errorf("failed to start task")
}
return nil
}
func (s *Node) Stop() error {
server := (*C.dqlite_node)(unsafe.Pointer(s))
if rc := C.dqlite_node_stop(server); rc != 0 {
return fmt.Errorf("task stopped with error code %d", rc)
}
return nil
}
// Close the server releasing all used resources.
func (s *Node) Close() {
server := (*C.dqlite_node)(unsafe.Pointer(s))
C.dqlite_node_destroy(server)
}
func (s *Node) Recover(cluster []protocol.NodeInfo) error {
server := (*C.dqlite_node)(unsafe.Pointer(s))
n := C.int(len(cluster))
infos := C.makeInfos(n)
defer C.free(unsafe.Pointer(infos))
for i, info := range cluster {
cid := C.unsigned(info.ID)
caddress := C.CString(info.Address)
defer C.free(unsafe.Pointer(caddress))
C.setInfo(infos, C.unsigned(i), cid, caddress)
}
if rc := C.dqlite_node_recover(server, infos, n); rc != 0 {
return fmt.Errorf("recover failed with error code %d", rc)
}
return nil
}
// Extract the underlying socket from a connection.
func connToSocket(conn net.Conn) (C.int, error) {
file, err := conn.(fileConn).File()
if err != nil {
return C.int(-1), err
}
fd1 := C.int(file.Fd())
// Duplicate the file descriptor, in order to prevent Go's finalizer to
// close it.
fd2 := C.dupCloexec(fd1)
if fd2 < 0 {
return C.int(-1), fmt.Errorf("failed to dup socket fd")
}
conn.Close()
return fd2, nil
}
// Interface that net.Conn must implement in order to extract the underlying
// file descriptor.
type fileConn interface {
File() (*os.File, error)
}
//export connectWithDial
func connectWithDial(handle C.uintptr_t, address *C.char, fd *C.int) C.int {
connectLock.Lock()
defer connectLock.Unlock()
dial := connectRegistry[handle]
// TODO: make timeout customizable.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
conn, err := dial(ctx, C.GoString(address))
if err != nil {
return C.RAFT_NOCONNECTION
}
socket, err := connToSocket(conn)
if err != nil {
return C.RAFT_NOCONNECTION
}
*fd = socket
return C.int(0)
}
// Use handles to avoid passing Go pointers to C.
var connectRegistry = make(map[C.uintptr_t]protocol.DialFunc)
var connectIndex C.uintptr_t = 100
var connectLock = sync.Mutex{}
// ErrNodeStopped is returned by Node.Handle() is the server was stopped.
var ErrNodeStopped = fmt.Errorf("server was stopped")
// To compare bool values.
var cfalse C.bool
package logging
import (
"fmt"
"testing"
)
// Func is a function that can be used for logging.
type Func func(Level, string, ...interface{})
// Test returns a logging function that forwards messages to the test logger.
func Test(t *testing.T) Func {
return func(l Level, format string, a ...interface{}) {
format = fmt.Sprintf("%s: %s", l.String(), format)
t.Logf(format, a...)
}
}
// Stdout returns a logging function that prints log messages on standard
// output.
func Stdout() Func {
return func(l Level, format string, a ...interface{}) {
format = fmt.Sprintf("%s: %s\n", l.String(), format)
fmt.Printf(format, a...)
}
}
package logging
// Level defines the logging level.
type Level int
// Available logging levels.
const (
Debug Level = iota
Info
Warn
Error
)
func (l Level) String() string {
switch l {
case Debug:
return "DEBUG"
case Info:
return "INFO"
case Warn:
return "WARN"
case Error:
return "ERROR"
default:
return "UNKNOWN"
}
}
package protocol
// Buffer for reading responses or writing requests.
type buffer struct {
Bytes []byte
Offset int
}
func (b *buffer) Advance(amount int) {
b.Offset += amount
}
package protocol
import (
"time"
"github.com/Rican7/retry/strategy"
)
// Config holds various configuration parameters for a dqlite client.
type Config struct {
Dial DialFunc // Network dialer.
AttemptTimeout time.Duration // Timeout for each individual Dial attempt.
RetryStrategies []strategy.Strategy // Strategies used for retrying to connect to a leader.
}
package protocol
import (
"context"
"encoding/binary"
"fmt"
"io"
"net"
"github.com/Rican7/retry"
"github.com/canonical/go-dqlite/internal/logging"
"github.com/pkg/errors"
)
// DialFunc is a function that can be used to establish a network connection.
type DialFunc func(context.Context, string) (net.Conn, error)
// Connector is in charge of creating a dqlite SQL client connected to the
// current leader of a cluster.
type Connector struct {
id uint64 // Conn ID to use when registering against the server.
store NodeStore // Used to get and update current cluster servers.
config Config // Connection parameters.
log logging.Func // Logging function.
}
// NewConnector returns a new connector that can be used by a dqlite driver to
// create new clients connected to a leader dqlite server.
func NewConnector(id uint64, store NodeStore, config Config, log logging.Func) *Connector {
connector := &Connector{
id: id,
store: store,
config: config,
log: log,
}
return connector
}
// Connect finds the leader server and returns a connection to it.
//
// If the connector is stopped before a leader is found, nil is returned.
func (c *Connector) Connect(ctx context.Context) (*Protocol, error) {
var protocol *Protocol
// The retry strategy should be configured to retry indefinitely, until
// the given context is done.
err := retry.Retry(func(attempt uint) error {
log := func(l logging.Level, format string, a ...interface{}) {
format += fmt.Sprintf(" attempt=%d", attempt)
c.log(l, fmt.Sprintf(format, a...))
}
select {
case <-ctx.Done():
// Stop retrying
return nil
default:
}
var err error
protocol, err = c.connectAttemptAll(ctx, log)
if err != nil {
log(logging.Debug, "connection failed err=%v", err)
return err
}
return nil
}, c.config.RetryStrategies...)
if err != nil {
// The retry strategy should never give up until success or
// context expiration.
panic("connect retry aborted unexpectedly")
}
if ctx.Err() != nil {
return nil, ErrNoAvailableLeader
}
return protocol, nil
}
// Make a single attempt to establish a connection to the leader server trying
// all addresses available in the store.
func (c *Connector) connectAttemptAll(ctx context.Context, log logging.Func) (*Protocol, error) {
servers, err := c.store.Get(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to get cluster servers")
}
// Make an attempt for each address until we find the leader.
for _, server := range servers {
log := func(l logging.Level, format string, a ...interface{}) {
format += fmt.Sprintf(" address=%s", server.Address)
log(l, fmt.Sprintf(format, a...))
}
ctx, cancel := context.WithTimeout(ctx, c.config.AttemptTimeout)
defer cancel()
version := VersionOne
protocol, leader, err := c.connectAttemptOne(ctx, server.Address, version)
if err == errBadProtocol {
version = VersionLegacy
protocol, leader, err = c.connectAttemptOne(ctx, server.Address, version)
}
if err != nil {
// This server is unavailable, try with the next target.
log(logging.Debug, "server connection failed err=%v", err)
continue
}
if protocol != nil {
// We found the leader
log(logging.Info, "connected")
return protocol, nil
}
if leader == "" {
// This server does not know who the current leader is,
// try with the next target.
continue
}
// If we get here, it means this server reported that another
// server is the leader, let's close the connection to this
// server and try with the suggested one.
//logger = logger.With(zap.String("leader", leader))
protocol, leader, err = c.connectAttemptOne(ctx, leader, version)
if err != nil {
// The leader reported by the previous server is
// unavailable, try with the next target.
//logger.Info("leader server connection failed", zap.String("err", err.Error()))
continue
}
if protocol == nil {
// The leader reported by the target server does not consider itself
// the leader, try with the next target.
//logger.Info("reported leader server is not the leader")
continue
}
log(logging.Info, "connected")
return protocol, nil
}
return nil, ErrNoAvailableLeader
}
// Connect establishes a connection with a dqlite node.
func Connect(ctx context.Context, dial DialFunc, address string, version uint64) (*Protocol, error) {
// Establish the connection.
conn, err := dial(ctx, address)
if err != nil {
return nil, errors.Wrap(err, "failed to establish network connection")
}
// Latest protocol version.
protocol := make([]byte, 8)
binary.LittleEndian.PutUint64(protocol, version)
// Perform the protocol handshake.
n, err := conn.Write(protocol)
if err != nil {
conn.Close()
return nil, errors.Wrap(err, "failed to send handshake")
}
if n != 8 {
conn.Close()
return nil, errors.Wrap(io.ErrShortWrite, "failed to send handshake")
}
return NewProtocol(version, conn), nil
}
// Connect to the given dqlite server and check if it's the leader.
//
// Return values:
//
// - Any failure is hit: -> nil, "", err
// - Target not leader and no leader known: -> nil, "", nil
// - Target not leader and leader known: -> nil, leader, nil
// - Target is the leader: -> server, "", nil
//
func (c *Connector) connectAttemptOne(ctx context.Context, address string, version uint64) (*Protocol, string, error) {
protocol, err := Connect(ctx, c.config.Dial, address, version)
if err != nil {
return nil, "", err
}
// Send the initial Leader request.
request := Message{}
request.Init(16)
response := Message{}
response.Init(512)
EncodeLeader(&request)
if err := protocol.Call(ctx, &request, &response); err != nil {
protocol.Close()
cause := errors.Cause(err)
// Best-effort detection of a pre-1.0 dqlite node: when sent
// version 1 it should close the connection immediately.
if _, ok := cause.(*net.OpError); ok || cause == io.EOF {
return nil, "", errBadProtocol
}
return nil, "", errors.Wrap(err, "failed to send Leader request")
}
_, leader, err := DecodeNodeCompat(protocol, &response)
if err != nil {
protocol.Close()
return nil, "", errors.Wrap(err, "failed to parse Node response")
}
switch leader {
case "":
// Currently this server does not know about any leader.
protocol.Close()
return nil, "", nil
case address:
// This server is the leader, register ourselves and return.
request.Reset()
response.Reset()
EncodeClient(&request, c.id)
if err := protocol.Call(ctx, &request, &response); err != nil {
protocol.Close()
return nil, "", errors.Wrap(err, "failed to send Conn request")
}
_, err := DecodeWelcome(&response)
if err != nil {
protocol.Close()
return nil, "", errors.Wrap(err, "failed to parse Welcome response")
}
// TODO: enable heartbeat
// protocol.heartbeatTimeout = time.Duration(heartbeatTimeout) * time.Millisecond
//go protocol.heartbeat()
return protocol, "", nil
default:
// This server claims to know who the current leader is.
protocol.Close()
return nil, leader, nil
}
}
var errBadProtocol = fmt.Errorf("bad protocol")
package protocol
// VersionOne is version 1 of the server protocol.
const VersionOne = uint64(1)
// VersionLegacy is the pre 1.0 dqlite server protocol version.
const VersionLegacy = uint64(0x86104dd760433fe5)
// SQLite datatype codes
const (
Integer = 1
Float = 2
Text = 3
Blob = 4
Null = 5
)
// Special data types for time values.
const (
UnixTime = 9
ISO8601 = 10
Boolean = 11
)
// Request types.
const (
RequestLeader = 0
RequestClient = 1
RequestHeartbeat = 2
RequestOpen = 3
RequestPrepare = 4
RequestExec = 5
RequestQuery = 6
RequestFinalize = 7
RequestExecSQL = 8
RequestQuerySQL = 9
RequestInterrupt = 10
RequestJoin = 12
RequestPromote = 13
RequestRemove = 14
RequestDump = 15
RequestCluster = 16
)
// Response types.
const (
ResponseFailure = 0
ResponseNode = 1
ResponseNodeLegacy = 1
ResponseWelcome = 2
ResponseNodes = 3
ResponseDb = 4
ResponseStmt = 5
ResponseResult = 6
ResponseRows = 7
ResponseEmpty = 8
ResponseFiles = 9
)
package protocol
import (
"context"
"net"
)
// TCPDial is a dial function using plain TCP to establish the network
// connection.
func TCPDial(ctx context.Context, address string) (net.Conn, error) {
dialer := net.Dialer{}
return dialer.DialContext(ctx, "tcp", address)
}
// UnixDial is a dial function using Unix sockets to establish the network
// connection.
func UnixDial(ctx context.Context, address string) (net.Conn, error) {
dialer := net.Dialer{}
return dialer.DialContext(ctx, "unix", address)
}
package protocol
import (
"fmt"
)
// Client errors.
var (
ErrNoAvailableLeader = fmt.Errorf("no available dqlite leader server found")
errStop = fmt.Errorf("connector was stopped")
errStaleLeader = fmt.Errorf("server has lost leadership")
errNotClustered = fmt.Errorf("server is not clustered")
errNegativeRead = fmt.Errorf("reader returned negative count from Read")
errMessageEOF = fmt.Errorf("message eof")
)
// ErrRequest is returned in case of request failure.
type ErrRequest struct {
Code uint64
Description string
}
func (e ErrRequest) Error() string {
return fmt.Sprintf("%s (%d)", e.Description, e.Code)
}
// ErrRowsPart is returned when the first batch of a multi-response result
// batch is done.
var ErrRowsPart = fmt.Errorf("not all rows were returned in this response")
package protocol
import (
"context"
"encoding/binary"
"io"
"net"
"sync"
"time"
"github.com/pkg/errors"
)
// Protocol sends and receive the dqlite message on the wire.
type Protocol struct {
version uint64 // Protocol version
conn net.Conn // Underlying network connection.
contextTimeout time.Duration // Default context timeout.
closeCh chan struct{} // Stops the heartbeat when the connection gets closed
mu sync.Mutex // Serialize requests
netErr error // A network error occurred
}
func NewProtocol(version uint64, conn net.Conn) *Protocol {
protocol := &Protocol{
version: version,
conn: conn,
closeCh: make(chan struct{}),
contextTimeout: 5 * time.Second,
}
return protocol
}
// SetContextTimeout sets the default context timeout when no deadline is
// provided.
func (p *Protocol) SetContextTimeout(timeout time.Duration) {
p.contextTimeout = timeout
}
// Call invokes a dqlite RPC, sending a request message and receiving a
// response message.
func (p *Protocol) Call(ctx context.Context, request, response *Message) (err error) {
// We need to take a lock since the dqlite server currently does not
// support concurrent requests.
p.mu.Lock()
defer p.mu.Unlock()
if p.netErr != nil {
return p.netErr
}
// Honor the ctx deadline, if present, or use a default.
deadline, ok := ctx.Deadline()
if !ok {
deadline = time.Now().Add(p.contextTimeout)
}
p.conn.SetDeadline(deadline)
if err = p.send(request); err != nil {
err = errors.Wrap(err, "failed to send request")
goto err
}
if err = p.recv(response); err != nil {
err = errors.Wrap(err, "failed to receive response")
goto err
}
return
err:
switch errors.Cause(err).(type) {
case *net.OpError:
p.netErr = err
}
return
}
// More is used when a request maps to multiple responses.
func (p *Protocol) More(ctx context.Context, response *Message) error {
return p.recv(response)
}
// Interrupt sends an interrupt request and awaits for the server's empty
// response.
func (p *Protocol) Interrupt(ctx context.Context, request *Message, response *Message) error {
// We need to take a lock since the dqlite server currently does not
// support concurrent requests.
p.mu.Lock()
defer p.mu.Unlock()
// Honor the ctx deadline, if present, or use a default.
deadline, ok := ctx.Deadline()
if !ok {
deadline = time.Now().Add(2 * time.Second)
}
p.conn.SetDeadline(deadline)
defer request.Reset()
EncodeInterrupt(request, 0)
if err := p.send(request); err != nil {
return errors.Wrap(err, "failed to send interrupt request")
}
for {
if err := p.recv(response); err != nil {
response.Reset()
return errors.Wrap(err, "failed to receive response")
}
mtype, _ := response.getHeader()
response.Reset()
if mtype == ResponseEmpty {
break
}
}
return nil
}
// Close the client connection.
func (p *Protocol) Close() error {
close(p.closeCh)
return p.conn.Close()
}
func (p *Protocol) send(req *Message) error {
if err := p.sendHeader(req); err != nil {
return errors.Wrap(err, "failed to send header")
}
if err := p.sendBody(req); err != nil {
return errors.Wrap(err, "failed to send body")
}
return nil
}
func (p *Protocol) sendHeader(req *Message) error {
n, err := p.conn.Write(req.header[:])
if err != nil {
return errors.Wrap(err, "failed to send header")
}
if n != messageHeaderSize {
return errors.Wrap(io.ErrShortWrite, "failed to send header")
}
return nil
}
func (p *Protocol) sendBody(req *Message) error {
buf := req.body1.Bytes[:req.body1.Offset]
n, err := p.conn.Write(buf)
if err != nil {
return errors.Wrap(err, "failed to send static body")
}
if n != len(buf) {
return errors.Wrap(io.ErrShortWrite, "failed to write body")
}
if req.body2.Bytes == nil {
return nil
}
buf = req.body2.Bytes[:req.body2.Offset]
n, err = p.conn.Write(buf)
if err != nil {
return errors.Wrap(err, "failed to send dynamic body")
}
if n != len(buf) {
return errors.Wrap(io.ErrShortWrite, "failed to write body")
}
return nil
}
func (p *Protocol) recv(res *Message) error {
if err := p.recvHeader(res); err != nil {
return errors.Wrap(err, "failed to receive header")
}
if err := p.recvBody(res); err != nil {
return errors.Wrap(err, "failed to receive body")
}
return nil
}
func (p *Protocol) recvHeader(res *Message) error {
if err := p.recvPeek(res.header); err != nil {
return errors.Wrap(err, "failed to receive header")
}
res.words = binary.LittleEndian.Uint32(res.header[0:])
res.mtype = res.header[4]
res.flags = res.header[5]
res.extra = binary.LittleEndian.Uint16(res.header[6:])
return nil
}
func (p *Protocol) recvBody(res *Message) error {
n := int(res.words) * messageWordSize
n1 := n
n2 := 0
if n1 > len(res.body1.Bytes) {
// We need to allocate the dynamic buffer.
n1 = len(res.body1.Bytes)
n2 = n - n1
}
buf := res.body1.Bytes[:n1]
if err := p.recvPeek(buf); err != nil {
return errors.Wrap(err, "failed to read body")
}
if n2 > 0 {
res.body2.Bytes = make([]byte, n2)
res.body2.Offset = 0
buf = res.body2.Bytes
if err := p.recvPeek(buf); err != nil {
return errors.Wrap(err, "failed to read body")
}
}
return nil
}
// Read until buf is full.
func (p *Protocol) recvPeek(buf []byte) error {
for offset := 0; offset < len(buf); {
n, err := p.recvFill(buf[offset:])
if err != nil {
return err
}
offset += n
}
return nil
}
// Try to fill buf, but perform at most one read.
func (p *Protocol) recvFill(buf []byte) (int, error) {
// Read new data: try a limited number of times.
//
// This technique is copied from bufio.Reader.
for i := messageMaxConsecutiveEmptyReads; i > 0; i-- {
n, err := p.conn.Read(buf)
if n < 0 {
panic(errNegativeRead)
}
if err != nil {
return -1, err
}
if n > 0 {
return n, nil
}
}
return -1, io.ErrNoProgress
}
/*
func (p *Protocol) heartbeat() {
request := Message{}
request.Init(16)
response := Message{}
response.Init(512)
for {
delay := c.heartbeatTimeout / 3
//c.logger.Debug("sending heartbeat", zap.Duration("delay", delay))
time.Sleep(delay)
// Check if we've been closed.
select {
case <-c.closeCh:
return
default:
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
EncodeHeartbeat(&request, uint64(time.Now().Unix()))
err := c.Call(ctx, &request, &response)
// We bail out upon failures.
//
// TODO: make the client survive temporary disconnections.
if err != nil {
cancel()
//c.logger.Error("heartbeat failed", zap.Error(err))
return
}
//addresses, err := DecodeNodes(&response)
_, err = DecodeNodes(&response)
if err != nil {
cancel()
//c.logger.Error("invalid heartbeat response", zap.Error(err))
return
}
// if err := c.store.Set(ctx, addresses); err != nil {
// cancel()
// c.logger.Error("failed to update servers", zap.Error(err))
// return
// }
cancel()
request.Reset()
response.Reset()
}
}
*/
// DecodeNodeCompat handles also pre-1.0 legacy server messages.
func DecodeNodeCompat(protocol *Protocol, response *Message) (uint64, string, error) {
if protocol.version == VersionLegacy {
address, err := DecodeNodeLegacy(response)
if err != nil {
return 0, "", err
}
return 0, address, nil
}
return DecodeNode(response)
}
package protocol
// DO NOT EDIT
//
// This file was generated by ./schema.sh
// EncodeLeader encodes a Leader request.
func EncodeLeader(request *Message) {
request.putUint64(0)
request.putHeader(RequestLeader)
}
// EncodeClient encodes a Client request.
func EncodeClient(request *Message, id uint64) {
request.putUint64(id)
request.putHeader(RequestClient)
}
// EncodeHeartbeat encodes a Heartbeat request.
func EncodeHeartbeat(request *Message, timestamp uint64) {
request.putUint64(timestamp)
request.putHeader(RequestHeartbeat)
}
// EncodeOpen encodes a Open request.
func EncodeOpen(request *Message, name string, flags uint64, vfs string) {
request.putString(name)
request.putUint64(flags)
request.putString(vfs)
request.putHeader(RequestOpen)
}
// EncodePrepare encodes a Prepare request.
func EncodePrepare(request *Message, db uint64, sql string) {
request.putUint64(db)
request.putString(sql)
request.putHeader(RequestPrepare)
}
// EncodeExec encodes a Exec request.
func EncodeExec(request *Message, db uint32, stmt uint32, values NamedValues) {
request.putUint32(db)
request.putUint32(stmt)
request.putNamedValues(values)
request.putHeader(RequestExec)
}
// EncodeQuery encodes a Query request.
func EncodeQuery(request *Message, db uint32, stmt uint32, values NamedValues) {
request.putUint32(db)
request.putUint32(stmt)
request.putNamedValues(values)
request.putHeader(RequestQuery)
}
// EncodeFinalize encodes a Finalize request.
func EncodeFinalize(request *Message, db uint32, stmt uint32) {
request.putUint32(db)
request.putUint32(stmt)
request.putHeader(RequestFinalize)
}
// EncodeExecSQL encodes a ExecSQL request.
func EncodeExecSQL(request *Message, db uint64, sql string, values NamedValues) {
request.putUint64(db)
request.putString(sql)
request.putNamedValues(values)
request.putHeader(RequestExecSQL)
}
// EncodeQuerySQL encodes a QuerySQL request.
func EncodeQuerySQL(request *Message, db uint64, sql string, values NamedValues) {
request.putUint64(db)
request.putString(sql)
request.putNamedValues(values)
request.putHeader(RequestQuerySQL)
}
// EncodeInterrupt encodes a Interrupt request.
func EncodeInterrupt(request *Message, db uint64) {
request.putUint64(db)
request.putHeader(RequestInterrupt)
}
// EncodeJoin encodes a Join request.
func EncodeJoin(request *Message, id uint64, address string) {
request.putUint64(id)
request.putString(address)
request.putHeader(RequestJoin)
}
// EncodePromote encodes a Promote request.
func EncodePromote(request *Message, id uint64) {
request.putUint64(id)
request.putHeader(RequestPromote)
}
// EncodeRemove encodes a Remove request.
func EncodeRemove(request *Message, id uint64) {
request.putUint64(id)
request.putHeader(RequestRemove)
}
// EncodeDump encodes a Dump request.
func EncodeDump(request *Message, name string) {
request.putString(name)
request.putHeader(RequestDump)
}
// EncodeCluster encodes a Cluster request.
func EncodeCluster(request *Message) {
request.putUint64(0)
request.putHeader(RequestCluster)
}
package protocol
// DO NOT EDIT
//
// This file was generated by ./schema.sh
import "fmt"
// DecodeFailure decodes a Failure response.
func DecodeFailure(response *Message) (code uint64, message string, err error) {
mtype, _ := response.getHeader()
if mtype == ResponseFailure {
e := ErrRequest{}
e.Code = response.getUint64()
e.Description = response.getString()
err = e
return
}
if mtype != ResponseFailure {
err = fmt.Errorf("unexpected response type %d", mtype)
return
}
code = response.getUint64()
message = response.getString()
return
}
// DecodeWelcome decodes a Welcome response.
func DecodeWelcome(response *Message) (heartbeatTimeout uint64, err error) {
mtype, _ := response.getHeader()
if mtype == ResponseFailure {
e := ErrRequest{}
e.Code = response.getUint64()
e.Description = response.getString()
err = e
return
}
if mtype != ResponseWelcome {
err = fmt.Errorf("unexpected response type %d", mtype)
return
}
heartbeatTimeout = response.getUint64()
return
}
// DecodeNodeLegacy decodes a NodeLegacy response.
func DecodeNodeLegacy(response *Message) (address string, err error) {
mtype, _ := response.getHeader()
if mtype == ResponseFailure {
e := ErrRequest{}
e.Code = response.getUint64()
e.Description = response.getString()
err = e
return
}
if mtype != ResponseNodeLegacy {
err = fmt.Errorf("unexpected response type %d", mtype)
return
}
address = response.getString()
return
}
// DecodeNode decodes a Node response.
func DecodeNode(response *Message) (id uint64, address string, err error) {
mtype, _ := response.getHeader()
if mtype == ResponseFailure {
e := ErrRequest{}
e.Code = response.getUint64()
e.Description = response.getString()
err = e
return
}
if mtype != ResponseNode {
err = fmt.Errorf("unexpected response type %d", mtype)
return
}
id = response.getUint64()
address = response.getString()
return
}
// DecodeNodes decodes a Nodes response.
func DecodeNodes(response *Message) (servers Nodes, err error) {
mtype, _ := response.getHeader()
if mtype == ResponseFailure {
e := ErrRequest{}
e.Code = response.getUint64()
e.Description = response.getString()
err = e
return
}
if mtype != ResponseNodes {
err = fmt.Errorf("unexpected response type %d", mtype)
return
}
servers = response.getNodes()
return
}
// DecodeDb decodes a Db response.
func DecodeDb(response *Message) (id uint32, err error) {
mtype, _ := response.getHeader()
if mtype == ResponseFailure {
e := ErrRequest{}
e.Code = response.getUint64()
e.Description = response.getString()
err = e
return
}
if mtype != ResponseDb {
err = fmt.Errorf("unexpected response type %d", mtype)
return
}
id = response.getUint32()
response.getUint32()
return
}
// DecodeStmt decodes a Stmt response.
func DecodeStmt(response *Message) (db uint32, id uint32, params uint64, err error) {
mtype, _ := response.getHeader()
if mtype == ResponseFailure {
e := ErrRequest{}
e.Code = response.getUint64()
e.Description = response.getString()
err = e
return
}
if mtype != ResponseStmt {
err = fmt.Errorf("unexpected response type %d", mtype)
return
}
db = response.getUint32()
id = response.getUint32()
params = response.getUint64()
return
}
// DecodeEmpty decodes a Empty response.
func DecodeEmpty(response *Message) (err error) {
mtype, _ := response.getHeader()
if mtype == ResponseFailure {
e := ErrRequest{}
e.Code = response.getUint64()
e.Description = response.getString()
err = e
return
}
if mtype != ResponseEmpty {
err = fmt.Errorf("unexpected response type %d", mtype)
return
}
response.getUint64()
return
}
// DecodeResult decodes a Result response.
func DecodeResult(response *Message) (result Result, err error) {
mtype, _ := response.getHeader()
if mtype == ResponseFailure {
e := ErrRequest{}
e.Code = response.getUint64()
e.Description = response.getString()
err = e
return
}
if mtype != ResponseResult {
err = fmt.Errorf("unexpected response type %d", mtype)
return
}
result = response.getResult()
return
}
// DecodeRows decodes a Rows response.
func DecodeRows(response *Message) (rows Rows, err error) {
mtype, _ := response.getHeader()
if mtype == ResponseFailure {
e := ErrRequest{}
e.Code = response.getUint64()
e.Description = response.getString()
err = e
return
}
if mtype != ResponseRows {
err = fmt.Errorf("unexpected response type %d", mtype)
return
}
rows = response.getRows()
return
}
// DecodeFiles decodes a Files response.
func DecodeFiles(response *Message) (files Files, err error) {
mtype, _ := response.getHeader()
if mtype == ResponseFailure {
e := ErrRequest{}
e.Code = response.getUint64()
e.Description = response.getString()
err = e
return
}
if mtype != ResponseFiles {
err = fmt.Errorf("unexpected response type %d", mtype)
return
}
files = response.getFiles()
return
}
package protocol
//go:generate ./schema.sh --request init
//go:generate ./schema.sh --request Leader unused:uint64
//go:generate ./schema.sh --request Client id:uint64
//go:generate ./schema.sh --request Heartbeat timestamp:uint64
//go:generate ./schema.sh --request Open name:string flags:uint64 vfs:string
//go:generate ./schema.sh --request Prepare db:uint64 sql:string
//go:generate ./schema.sh --request Exec db:uint32 stmt:uint32 values:NamedValues
//go:generate ./schema.sh --request Query db:uint32 stmt:uint32 values:NamedValues
//go:generate ./schema.sh --request Finalize db:uint32 stmt:uint32
//go:generate ./schema.sh --request ExecSQL db:uint64 sql:string values:NamedValues
//go:generate ./schema.sh --request QuerySQL db:uint64 sql:string values:NamedValues
//go:generate ./schema.sh --request Interrupt db:uint64
//go:generate ./schema.sh --request Join id:uint64 address:string
//go:generate ./schema.sh --request Promote id:uint64
//go:generate ./schema.sh --request Remove id:uint64
//go:generate ./schema.sh --request Dump name:string
//go:generate ./schema.sh --request Cluster unused:uint64
//go:generate ./schema.sh --response init
//go:generate ./schema.sh --response Failure code:uint64 message:string
//go:generate ./schema.sh --response Welcome heartbeatTimeout:uint64
//go:generate ./schema.sh --response NodeLegacy address:string
//go:generate ./schema.sh --response Node id:uint64 address:string
//go:generate ./schema.sh --response Nodes servers:Nodes
//go:generate ./schema.sh --response Db id:uint32 unused:uint32
//go:generate ./schema.sh --response Stmt db:uint32 id:uint32 params:uint64
//go:generate ./schema.sh --response Empty unused:uint64
//go:generate ./schema.sh --response Result result:Result
//go:generate ./schema.sh --response Rows rows:Rows
//go:generate ./schema.sh --response Files files:Files
#!/bin/bash
request_init() {
cat > request.go <<EOF
package protocol
// DO NOT EDIT
//
// This file was generated by ./schema.sh
EOF
}
response_init() {
cat > response.go <<EOF
package protocol
// DO NOT EDIT
//
// This file was generated by ./schema.sh
import "fmt"
EOF
}
entity=$1
shift
cmd=$1
shift
if [ "$entity" = "--request" ]; then
if [ "$cmd" = "init" ]; then
request_init
exit
fi
args=""
for i in "${@}"
do
name=$(echo "$i" | cut -f 1 -d :)
type=$(echo "$i" | cut -f 2 -d :)
if [ "$name" = "unused" ]; then
continue
fi
args=$(echo "${args}, ${name} ${type}")
done
cat >> request.go <<EOF
// Encode${cmd} encodes a $cmd request.
func Encode${cmd}(request *Message${args}) {
EOF
for i in "${@}"
do
name=$(echo "$i" | cut -f 1 -d :)
type=$(echo "$i" | cut -f 2 -d :)
if [ "$name" = "unused" ]; then
name=$(echo "0")
fi
cat >> request.go <<EOF
request.put${type^}(${name})
EOF
done
cat >> request.go <<EOF
request.putHeader(Request${cmd})
}
EOF
fi
if [ "$entity" = "--response" ]; then
if [ "$cmd" = "init" ]; then
response_init
exit
fi
returns=""
for i in "${@}"
do
name=$(echo "$i" | cut -f 1 -d :)
type=$(echo "$i" | cut -f 2 -d :)
if [ "$name" = "unused" ]; then
continue
fi
returns=$(echo "${returns}${name} ${type}, ")
done
cat >> response.go <<EOF
// Decode${cmd} decodes a $cmd response.
func Decode${cmd}(response *Message) (${returns}err error) {
mtype, _ := response.getHeader()
if mtype == ResponseFailure {
e := ErrRequest{}
e.Code = response.getUint64()
e.Description = response.getString()
err = e
return
}
if mtype != Response${cmd} {
err = fmt.Errorf("unexpected response type %d", mtype)
return
}
EOF
for i in "${@}"
do
name=$(echo "$i" | cut -f 1 -d :)
type=$(echo "$i" | cut -f 2 -d :)
assign=$(echo "${name} = ")
if [ "$name" = "unused" ]; then
assign=$(echo "")
fi
cat >> response.go <<EOF
${assign}response.get${type^}()
EOF
done
cat >> response.go <<EOF
return
}
EOF
fi
package protocol
import (
"context"
)
// NodeInfo holds information about a single server.
type NodeInfo struct {
ID uint64
Address string
}
// NodeStore is used by a dqlite client to get an initial list of candidate
// dqlite servers that it can dial in order to find a leader server to connect
// to.
//
// Once connected, the client periodically updates the server addresses in the
// store by querying the leader about changes in the cluster (such as servers
// being added or removed).
type NodeStore interface {
// Get return the list of known servers.
Get(context.Context) ([]NodeInfo, error)
// Set updates the list of known cluster servers.
Set(context.Context, []NodeInfo) error
}
// InmemNodeStore keeps the list of servers in memory.
type InmemNodeStore struct {
servers []NodeInfo
}
// NewInmemNodeStore creates NodeStore which stores its data in-memory.
func NewInmemNodeStore() *InmemNodeStore {
return &InmemNodeStore{
servers: make([]NodeInfo, 0),
}
}
// Get the current servers.
func (i *InmemNodeStore) Get(ctx context.Context) ([]NodeInfo, error) {
return i.servers, nil
}
// Set the servers.
func (i *InmemNodeStore) Set(ctx context.Context, servers []NodeInfo) error {
i.servers = servers
return nil
}
package dqlite
import (
"time"
"github.com/canonical/go-dqlite/client"
"github.com/canonical/go-dqlite/internal/bindings"
"github.com/pkg/errors"
)
// Node runs a dqlite node.
type Node struct {
log client.LogFunc // Logger
server *bindings.Node // Low-level C implementation
acceptCh chan error // Receives connection handling errors
id uint64
address string
bindAddress string
}
// NodeInfo is a convenience alias for client.NodeInfo.
type NodeInfo = client.NodeInfo
// Option can be used to tweak node parameters.
type Option func(*options)
// WithDialFunc sets a custom dial function for the server.
func WithDialFunc(dial client.DialFunc) Option {
return func(options *options) {
options.DialFunc = dial
}
}
// WithBindAddress sets a custom bind address for the server.
func WithBindAddress(address string) Option {
return func(options *options) {
options.BindAddress = address
}
}
// WithNetworkLatency sets the average one-way network latency.
func WithNetworkLatency(latency time.Duration) Option {
return func(options *options) {
options.NetworkLatency = uint64(latency.Nanoseconds())
}
}
// New creates a new Node instance.
func New(id uint64, address string, dir string, options ...Option) (*Node, error) {
o := defaultOptions()
for _, option := range options {
option(o)
}
server, err := bindings.NewNode(id, address, dir)
if err != nil {
return nil, err
}
if o.DialFunc != nil {
if err := server.SetDialFunc(o.DialFunc); err != nil {
return nil, err
}
}
if o.BindAddress != "" {
if err := server.SetBindAddress(o.BindAddress); err != nil {
return nil, err
}
}
if o.NetworkLatency != 0 {
if err := server.SetNetworkLatency(o.NetworkLatency); err != nil {
return nil, err
}
}
s := &Node{
server: server,
acceptCh: make(chan error, 1),
id: id,
address: address,
bindAddress: o.BindAddress,
}
return s, nil
}
// BindAddress returns the network address the node is listening to.
func (s *Node) BindAddress() string {
return s.server.GetBindAddress()
}
// Start serving requests.
func (s *Node) Start() error {
return s.server.Start()
}
// Recover a node by forcing a new cluster configuration.
func (s *Node) Recover(cluster []NodeInfo) error {
return s.server.Recover(cluster)
}
// Hold configuration options for a dqlite server.
type options struct {
Log client.LogFunc
DialFunc client.DialFunc
BindAddress string
NetworkLatency uint64
}
// Close the server, releasing all resources it created.
func (s *Node) Close() error {
// Send a stop signal to the dqlite event loop.
if err := s.server.Stop(); err != nil {
return errors.Wrap(err, "server failed to stop")
}
s.server.Close()
return nil
}
// Create a options object with sane defaults.
func defaultOptions() *options {
return &options{
DialFunc: client.DefaultDialFunc,
}
}
...@@ -32,7 +32,7 @@ import ( ...@@ -32,7 +32,7 @@ import (
func init() { func init() {
initHijack() initHijack()
// Go `log` pacakge uses os.Stderr. // Go `log` package uses os.Stderr.
SetFormatter(NewDefaultFormatter(os.Stderr)) SetFormatter(NewDefaultFormatter(os.Stderr))
SetGlobalLogLevel(INFO) SetGlobalLogLevel(INFO)
} }
......
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
.idea
.vscode
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
.project
EBNF.txt
test1.tpl
pongo2_internal_test.go
tpl-error.out
/count.out
/cover.out
*.swp
*.iml
/cpu.out
/mem.out
/pongo2.test
*.error
/profile
/coverage.out
/pongo2_internal_test.ignore
go.sum
language: go
os:
- linux
- osx
go:
- 1.12
script:
- go test -v
Main author and maintainer of pongo2:
* Florian Schlachter <flori@n-schlachter.de>
Contributors (in no specific order):
* @romanoaugusto88
* @vitalbh
* @blaubaer
Feel free to add yourself to the list or to modify your entry if you did a contribution.
The MIT License (MIT)
Copyright (c) 2013-2014 Florian Schlachter
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.
package pongo2
import (
"regexp"
"github.com/juju/errors"
)
var reIdentifiers = regexp.MustCompile("^[a-zA-Z0-9_]+$")
var autoescape = true
func SetAutoescape(newValue bool) {
autoescape = newValue
}
// A Context type provides constants, variables, instances or functions to a template.
//
// pongo2 automatically provides meta-information or functions through the "pongo2"-key.
// Currently, context["pongo2"] contains the following keys:
// 1. version: returns the version string
//
// Template examples for accessing items from your context:
// {{ myconstant }}
// {{ myfunc("test", 42) }}
// {{ user.name }}
// {{ pongo2.version }}
type Context map[string]interface{}
func (c Context) checkForValidIdentifiers() *Error {
for k, v := range c {
if !reIdentifiers.MatchString(k) {
return &Error{
Sender: "checkForValidIdentifiers",
OrigError: errors.Errorf("context-key '%s' (value: '%+v') is not a valid identifier", k, v),
}
}
}
return nil
}
// Update updates this context with the key/value-pairs from another context.
func (c Context) Update(other Context) Context {
for k, v := range other {
c[k] = v
}
return c
}
// ExecutionContext contains all data important for the current rendering state.
//
// If you're writing a custom tag, your tag's Execute()-function will
// have access to the ExecutionContext. This struct stores anything
// about the current rendering process's Context including
// the Context provided by the user (field Public).
// You can safely use the Private context to provide data to the user's
// template (like a 'forloop'-information). The Shared-context is used
// to share data between tags. All ExecutionContexts share this context.
//
// Please be careful when accessing the Public data.
// PLEASE DO NOT MODIFY THE PUBLIC CONTEXT (read-only).
//
// To create your own execution context within tags, use the
// NewChildExecutionContext(parent) function.
type ExecutionContext struct {
template *Template
Autoescape bool
Public Context
Private Context
Shared Context
}
var pongo2MetaContext = Context{
"version": Version,
}
func newExecutionContext(tpl *Template, ctx Context) *ExecutionContext {
privateCtx := make(Context)
// Make the pongo2-related funcs/vars available to the context
privateCtx["pongo2"] = pongo2MetaContext
return &ExecutionContext{
template: tpl,
Public: ctx,
Private: privateCtx,
Autoescape: autoescape,
}
}
func NewChildExecutionContext(parent *ExecutionContext) *ExecutionContext {
newctx := &ExecutionContext{
template: parent.template,
Public: parent.Public,
Private: make(Context),
Autoescape: parent.Autoescape,
}
newctx.Shared = parent.Shared
// Copy all existing private items
newctx.Private.Update(parent.Private)
return newctx
}
func (ctx *ExecutionContext) Error(msg string, token *Token) *Error {
return ctx.OrigError(errors.New(msg), token)
}
func (ctx *ExecutionContext) OrigError(err error, token *Token) *Error {
filename := ctx.template.name
var line, col int
if token != nil {
// No tokens available
// TODO: Add location (from where?)
filename = token.Filename
line = token.Line
col = token.Col
}
return &Error{
Template: ctx.template,
Filename: filename,
Line: line,
Column: col,
Token: token,
Sender: "execution",
OrigError: err,
}
}
func (ctx *ExecutionContext) Logf(format string, args ...interface{}) {
ctx.template.set.logf(format, args...)
}
// A Django-syntax like template-engine
//
// Blog posts about pongo2 (including introduction and migration):
// https://www.florian-schlachter.de/?tag=pongo2
//
// Complete documentation on the template language:
// https://docs.djangoproject.com/en/dev/topics/templates/
//
// Try out pongo2 live in the pongo2 playground:
// https://www.florian-schlachter.de/pongo2/
//
// Make sure to read README.md in the repository as well.
//
// A tiny example with template strings:
//
// (Snippet on playground: https://www.florian-schlachter.de/pongo2/?id=1206546277)
//
// // Compile the template first (i. e. creating the AST)
// tpl, err := pongo2.FromString("Hello {{ name|capfirst }}!")
// if err != nil {
// panic(err)
// }
// // Now you can render the template with the given
// // pongo2.Context how often you want to.
// out, err := tpl.Execute(pongo2.Context{"name": "fred"})
// if err != nil {
// panic(err)
// }
// fmt.Println(out) // Output: Hello Fred!
//
package pongo2
package pongo2
import (
"bufio"
"fmt"
"os"
)
// The Error type is being used to address an error during lexing, parsing or
// execution. If you want to return an error object (for example in your own
// tag or filter) fill this object with as much information as you have.
// Make sure "Sender" is always given (if you're returning an error within
// a filter, make Sender equals 'filter:yourfilter'; same goes for tags: 'tag:mytag').
// It's okay if you only fill in ErrorMsg if you don't have any other details at hand.
type Error struct {
Template *Template
Filename string
Line int
Column int
Token *Token
Sender string
OrigError error
}
func (e *Error) updateFromTokenIfNeeded(template *Template, t *Token) *Error {
if e.Template == nil {
e.Template = template
}
if e.Token == nil {
e.Token = t
if e.Line <= 0 {
e.Line = t.Line
e.Column = t.Col
}
}
return e
}
// Returns a nice formatted error string.
func (e *Error) Error() string {
s := "[Error"
if e.Sender != "" {
s += " (where: " + e.Sender + ")"
}
if e.Filename != "" {
s += " in " + e.Filename
}
if e.Line > 0 {
s += fmt.Sprintf(" | Line %d Col %d", e.Line, e.Column)
if e.Token != nil {
s += fmt.Sprintf(" near '%s'", e.Token.Val)
}
}
s += "] "
s += e.OrigError.Error()
return s
}
// RawLine returns the affected line from the original template, if available.
func (e *Error) RawLine() (line string, available bool, outErr error) {
if e.Line <= 0 || e.Filename == "<string>" {
return "", false, nil
}
filename := e.Filename
if e.Template != nil {
filename = e.Template.set.resolveFilename(e.Template, e.Filename)
}
file, err := os.Open(filename)
if err != nil {
return "", false, err
}
defer func() {
err := file.Close()
if err != nil && outErr == nil {
outErr = err
}
}()
scanner := bufio.NewScanner(file)
l := 0
for scanner.Scan() {
l++
if l == e.Line {
return scanner.Text(), true, nil
}
}
return "", false, nil
}
package pongo2
import (
"fmt"
"github.com/juju/errors"
)
// FilterFunction is the type filter functions must fulfil
type FilterFunction func(in *Value, param *Value) (out *Value, err *Error)
var filters map[string]FilterFunction
func init() {
filters = make(map[string]FilterFunction)
}
// FilterExists returns true if the given filter is already registered
func FilterExists(name string) bool {
_, existing := filters[name]
return existing
}
// RegisterFilter registers a new filter. If there's already a filter with the same
// name, RegisterFilter will panic. You usually want to call this
// function in the filter's init() function:
// http://golang.org/doc/effective_go.html#init
//
// See http://www.florian-schlachter.de/post/pongo2/ for more about
// writing filters and tags.
func RegisterFilter(name string, fn FilterFunction) error {
if FilterExists(name) {
return errors.Errorf("filter with name '%s' is already registered", name)
}
filters[name] = fn
return nil
}
// ReplaceFilter replaces an already registered filter with a new implementation. Use this
// function with caution since it allows you to change existing filter behaviour.
func ReplaceFilter(name string, fn FilterFunction) error {
if !FilterExists(name) {
return errors.Errorf("filter with name '%s' does not exist (therefore cannot be overridden)", name)
}
filters[name] = fn
return nil
}
// MustApplyFilter behaves like ApplyFilter, but panics on an error.
func MustApplyFilter(name string, value *Value, param *Value) *Value {
val, err := ApplyFilter(name, value, param)
if err != nil {
panic(err)
}
return val
}
// ApplyFilter applies a filter to a given value using the given parameters.
// Returns a *pongo2.Value or an error.
func ApplyFilter(name string, value *Value, param *Value) (*Value, *Error) {
fn, existing := filters[name]
if !existing {
return nil, &Error{
Sender: "applyfilter",
OrigError: errors.Errorf("Filter with name '%s' not found.", name),
}
}
// Make sure param is a *Value
if param == nil {
param = AsValue(nil)
}
return fn(value, param)
}
type filterCall struct {
token *Token
name string
parameter IEvaluator
filterFunc FilterFunction
}
func (fc *filterCall) Execute(v *Value, ctx *ExecutionContext) (*Value, *Error) {
var param *Value
var err *Error
if fc.parameter != nil {
param, err = fc.parameter.Evaluate(ctx)
if err != nil {
return nil, err
}
} else {
param = AsValue(nil)
}
filteredValue, err := fc.filterFunc(v, param)
if err != nil {
return nil, err.updateFromTokenIfNeeded(ctx.template, fc.token)
}
return filteredValue, nil
}
// Filter = IDENT | IDENT ":" FilterArg | IDENT "|" Filter
func (p *Parser) parseFilter() (*filterCall, *Error) {
identToken := p.MatchType(TokenIdentifier)
// Check filter ident
if identToken == nil {
return nil, p.Error("Filter name must be an identifier.", nil)
}
filter := &filterCall{
token: identToken,
name: identToken.Val,
}
// Get the appropriate filter function and bind it
filterFn, exists := filters[identToken.Val]
if !exists {
return nil, p.Error(fmt.Sprintf("Filter '%s' does not exist.", identToken.Val), identToken)
}
filter.filterFunc = filterFn
// Check for filter-argument (2 tokens needed: ':' ARG)
if p.Match(TokenSymbol, ":") != nil {
if p.Peek(TokenSymbol, "}}") != nil {
return nil, p.Error("Filter parameter required after ':'.", nil)
}
// Get filter argument expression
v, err := p.parseVariableOrLiteral()
if err != nil {
return nil, err
}
filter.parameter = v
}
return filter, nil
}
module github.com/flosch/pongo2
require (
github.com/go-check/check v0.0.0-20180628173108-788fd7840127
github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5
github.com/juju/loggo v0.0.0-20180524022052-584905176618 // indirect
github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/mattn/goveralls v0.0.2 // indirect
golang.org/x/tools v0.0.0-20181221001348-537d06c36207 // indirect
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce // indirect
gopkg.in/yaml.v2 v2.2.2 // indirect
)
package pongo2
func max(a, b int) int {
if a > b {
return a
}
return b
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
package pongo2
import (
"fmt"
"strings"
"unicode/utf8"
"github.com/juju/errors"
)
const (
TokenError = iota
EOF
TokenHTML
TokenKeyword
TokenIdentifier
TokenString
TokenNumber
TokenSymbol
)
var (
tokenSpaceChars = " \n\r\t"
tokenIdentifierChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_"
tokenIdentifierCharsWithDigits = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789"
tokenDigits = "0123456789"
// Available symbols in pongo2 (within filters/tag)
TokenSymbols = []string{
// 3-Char symbols
"{{-", "-}}", "{%-", "-%}",
// 2-Char symbols
"==", ">=", "<=", "&&", "||", "{{", "}}", "{%", "%}", "!=", "<>",
// 1-Char symbol
"(", ")", "+", "-", "*", "<", ">", "/", "^", ",", ".", "!", "|", ":", "=", "%",
}
// Available keywords in pongo2
TokenKeywords = []string{"in", "and", "or", "not", "true", "false", "as", "export"}
)
type TokenType int
type Token struct {
Filename string
Typ TokenType
Val string
Line int
Col int
TrimWhitespaces bool
}
type lexerStateFn func() lexerStateFn
type lexer struct {
name string
input string
start int // start pos of the item
pos int // current pos
width int // width of last rune
tokens []*Token
errored bool
startline int
startcol int
line int
col int
inVerbatim bool
verbatimName string
}
func (t *Token) String() string {
val := t.Val
if len(val) > 1000 {
val = fmt.Sprintf("%s...%s", val[:10], val[len(val)-5:len(val)])
}
typ := ""
switch t.Typ {
case TokenHTML:
typ = "HTML"
case TokenError:
typ = "Error"
case TokenIdentifier:
typ = "Identifier"
case TokenKeyword:
typ = "Keyword"
case TokenNumber:
typ = "Number"
case TokenString:
typ = "String"
case TokenSymbol:
typ = "Symbol"
default:
typ = "Unknown"
}
return fmt.Sprintf("<Token Typ=%s (%d) Val='%s' Line=%d Col=%d, WT=%t>",
typ, t.Typ, val, t.Line, t.Col, t.TrimWhitespaces)
}
func lex(name string, input string) ([]*Token, *Error) {
l := &lexer{
name: name,
input: input,
tokens: make([]*Token, 0, 100),
line: 1,
col: 1,
startline: 1,
startcol: 1,
}
l.run()
if l.errored {
errtoken := l.tokens[len(l.tokens)-1]
return nil, &Error{
Filename: name,
Line: errtoken.Line,
Column: errtoken.Col,
Sender: "lexer",
OrigError: errors.New(errtoken.Val),
}
}
return l.tokens, nil
}
func (l *lexer) value() string {
return l.input[l.start:l.pos]
}
func (l *lexer) length() int {
return l.pos - l.start
}
func (l *lexer) emit(t TokenType) {
tok := &Token{
Filename: l.name,
Typ: t,
Val: l.value(),
Line: l.startline,
Col: l.startcol,
}
if t == TokenString {
// Escape sequence \" in strings
tok.Val = strings.Replace(tok.Val, `\"`, `"`, -1)
tok.Val = strings.Replace(tok.Val, `\\`, `\`, -1)
}
if t == TokenSymbol && len(tok.Val) == 3 && (strings.HasSuffix(tok.Val, "-") || strings.HasPrefix(tok.Val, "-")) {
tok.TrimWhitespaces = true
tok.Val = strings.Replace(tok.Val, "-", "", -1)
}
l.tokens = append(l.tokens, tok)
l.start = l.pos
l.startline = l.line
l.startcol = l.col
}
func (l *lexer) next() rune {
if l.pos >= len(l.input) {
l.width = 0
return EOF
}
r, w := utf8.DecodeRuneInString(l.input[l.pos:])
l.width = w
l.pos += l.width
l.col += l.width
return r
}
func (l *lexer) backup() {
l.pos -= l.width
l.col -= l.width
}
func (l *lexer) peek() rune {
r := l.next()
l.backup()
return r
}
func (l *lexer) ignore() {
l.start = l.pos
l.startline = l.line
l.startcol = l.col
}
func (l *lexer) accept(what string) bool {
if strings.IndexRune(what, l.next()) >= 0 {
return true
}
l.backup()
return false
}
func (l *lexer) acceptRun(what string) {
for strings.IndexRune(what, l.next()) >= 0 {
}
l.backup()
}
func (l *lexer) errorf(format string, args ...interface{}) lexerStateFn {
t := &Token{
Filename: l.name,
Typ: TokenError,
Val: fmt.Sprintf(format, args...),
Line: l.startline,
Col: l.startcol,
}
l.tokens = append(l.tokens, t)
l.errored = true
l.startline = l.line
l.startcol = l.col
return nil
}
func (l *lexer) eof() bool {
return l.start >= len(l.input)-1
}
func (l *lexer) run() {
for {
// TODO: Support verbatim tag names
// https://docs.djangoproject.com/en/dev/ref/templates/builtins/#verbatim
if l.inVerbatim {
name := l.verbatimName
if name != "" {
name += " "
}
if strings.HasPrefix(l.input[l.pos:], fmt.Sprintf("{%% endverbatim %s%%}", name)) { // end verbatim
if l.pos > l.start {
l.emit(TokenHTML)
}
w := len("{% endverbatim %}")
l.pos += w
l.col += w
l.ignore()
l.inVerbatim = false
}
} else if strings.HasPrefix(l.input[l.pos:], "{% verbatim %}") { // tag
if l.pos > l.start {
l.emit(TokenHTML)
}
l.inVerbatim = true
w := len("{% verbatim %}")
l.pos += w
l.col += w
l.ignore()
}
if !l.inVerbatim {
// Ignore single-line comments {# ... #}
if strings.HasPrefix(l.input[l.pos:], "{#") {
if l.pos > l.start {
l.emit(TokenHTML)
}
l.pos += 2 // pass '{#'
l.col += 2
for {
switch l.peek() {
case EOF:
l.errorf("Single-line comment not closed.")
return
case '\n':
l.errorf("Newline not permitted in a single-line comment.")
return
}
if strings.HasPrefix(l.input[l.pos:], "#}") {
l.pos += 2 // pass '#}'
l.col += 2
break
}
l.next()
}
l.ignore() // ignore whole comment
// Comment skipped
continue // next token
}
if strings.HasPrefix(l.input[l.pos:], "{{") || // variable
strings.HasPrefix(l.input[l.pos:], "{%") { // tag
if l.pos > l.start {
l.emit(TokenHTML)
}
l.tokenize()
if l.errored {
return
}
continue
}
}
switch l.peek() {
case '\n':
l.line++
l.col = 0
}
if l.next() == EOF {
break
}
}
if l.pos > l.start {
l.emit(TokenHTML)
}
if l.inVerbatim {
l.errorf("verbatim-tag not closed, got EOF.")
}
}
func (l *lexer) tokenize() {
for state := l.stateCode; state != nil; {
state = state()
}
}
func (l *lexer) stateCode() lexerStateFn {
outer_loop:
for {
switch {
case l.accept(tokenSpaceChars):
if l.value() == "\n" {
return l.errorf("Newline not allowed within tag/variable.")
}
l.ignore()
continue
case l.accept(tokenIdentifierChars):
return l.stateIdentifier
case l.accept(tokenDigits):
return l.stateNumber
case l.accept(`"'`):
return l.stateString
}
// Check for symbol
for _, sym := range TokenSymbols {
if strings.HasPrefix(l.input[l.start:], sym) {
l.pos += len(sym)
l.col += l.length()
l.emit(TokenSymbol)
if sym == "%}" || sym == "-%}" || sym == "}}" || sym == "-}}" {
// Tag/variable end, return after emit
return nil
}
continue outer_loop
}
}
break
}
// Normal shut down
return nil
}
func (l *lexer) stateIdentifier() lexerStateFn {
l.acceptRun(tokenIdentifierChars)
l.acceptRun(tokenIdentifierCharsWithDigits)
for _, kw := range TokenKeywords {
if kw == l.value() {
l.emit(TokenKeyword)
return l.stateCode
}
}
l.emit(TokenIdentifier)
return l.stateCode
}
func (l *lexer) stateNumber() lexerStateFn {
l.acceptRun(tokenDigits)
if l.accept(tokenIdentifierCharsWithDigits) {
// This seems to be an identifier starting with a number.
// See https://github.com/flosch/pongo2/issues/151
return l.stateIdentifier()
}
/*
Maybe context-sensitive number lexing?
* comments.0.Text // first comment
* usercomments.1.0 // second user, first comment
* if (score >= 8.5) // 8.5 as a number
if l.peek() == '.' {
l.accept(".")
if !l.accept(tokenDigits) {
return l.errorf("Malformed number.")
}
l.acceptRun(tokenDigits)
}
*/
l.emit(TokenNumber)
return l.stateCode
}
func (l *lexer) stateString() lexerStateFn {
quotationMark := l.value()
l.ignore()
l.startcol-- // we're starting the position at the first "
for !l.accept(quotationMark) {
switch l.next() {
case '\\':
// escape sequence
switch l.peek() {
case '"', '\\':
l.next()
default:
return l.errorf("Unknown escape sequence: \\%c", l.peek())
}
case EOF:
return l.errorf("Unexpected EOF, string not closed.")
case '\n':
return l.errorf("Newline in string is not allowed.")
}
}
l.backup()
l.emit(TokenString)
l.next()
l.ignore()
return l.stateCode
}
package pongo2
// The root document
type nodeDocument struct {
Nodes []INode
}
func (doc *nodeDocument) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
for _, n := range doc.Nodes {
err := n.Execute(ctx, writer)
if err != nil {
return err
}
}
return nil
}
package pongo2
import (
"strings"
)
type nodeHTML struct {
token *Token
trimLeft bool
trimRight bool
}
func (n *nodeHTML) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
res := n.token.Val
if n.trimLeft {
res = strings.TrimLeft(res, tokenSpaceChars)
}
if n.trimRight {
res = strings.TrimRight(res, tokenSpaceChars)
}
writer.WriteString(res)
return nil
}
package pongo2
type NodeWrapper struct {
Endtag string
nodes []INode
}
func (wrapper *NodeWrapper) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
for _, n := range wrapper.nodes {
err := n.Execute(ctx, writer)
if err != nil {
return err
}
}
return nil
}
package pongo2
// Options allow you to change the behavior of template-engine.
// You can change the options before calling the Execute method.
type Options struct {
// If this is set to true the first newline after a block is removed (block, not variable tag!). Defaults to false.
TrimBlocks bool
// If this is set to true leading spaces and tabs are stripped from the start of a line to a block. Defaults to false
LStripBlocks bool
}
func newOptions() *Options {
return &Options{
TrimBlocks: false,
LStripBlocks: false,
}
}
// Update updates this options from another options.
func (opt *Options) Update(other *Options) *Options {
opt.TrimBlocks = other.TrimBlocks
opt.LStripBlocks = other.LStripBlocks
return opt
}
package pongo2
import (
"fmt"
"strings"
"github.com/juju/errors"
)
type INode interface {
Execute(*ExecutionContext, TemplateWriter) *Error
}
type IEvaluator interface {
INode
GetPositionToken() *Token
Evaluate(*ExecutionContext) (*Value, *Error)
FilterApplied(name string) bool
}
// The parser provides you a comprehensive and easy tool to
// work with the template document and arguments provided by
// the user for your custom tag.
//
// The parser works on a token list which will be provided by pongo2.
// A token is a unit you can work with. Tokens are either of type identifier,
// string, number, keyword, HTML or symbol.
//
// (See Token's documentation for more about tokens)
type Parser struct {
name string
idx int
tokens []*Token
lastToken *Token
// if the parser parses a template document, here will be
// a reference to it (needed to access the template through Tags)
template *Template
}
// Creates a new parser to parse tokens.
// Used inside pongo2 to parse documents and to provide an easy-to-use
// parser for tag authors
func newParser(name string, tokens []*Token, template *Template) *Parser {
p := &Parser{
name: name,
tokens: tokens,
template: template,
}
if len(tokens) > 0 {
p.lastToken = tokens[len(tokens)-1]
}
return p
}
// Consume one token. It will be gone forever.
func (p *Parser) Consume() {
p.ConsumeN(1)
}
// Consume N tokens. They will be gone forever.
func (p *Parser) ConsumeN(count int) {
p.idx += count
}
// Returns the current token.
func (p *Parser) Current() *Token {
return p.Get(p.idx)
}
// Returns the CURRENT token if the given type matches.
// Consumes this token on success.
func (p *Parser) MatchType(typ TokenType) *Token {
if t := p.PeekType(typ); t != nil {
p.Consume()
return t
}
return nil
}
// Returns the CURRENT token if the given type AND value matches.
// Consumes this token on success.
func (p *Parser) Match(typ TokenType, val string) *Token {
if t := p.Peek(typ, val); t != nil {
p.Consume()
return t
}
return nil
}
// Returns the CURRENT token if the given type AND *one* of
// the given values matches.
// Consumes this token on success.
func (p *Parser) MatchOne(typ TokenType, vals ...string) *Token {
for _, val := range vals {
if t := p.Peek(typ, val); t != nil {
p.Consume()
return t
}
}
return nil
}
// Returns the CURRENT token if the given type matches.
// It DOES NOT consume the token.
func (p *Parser) PeekType(typ TokenType) *Token {
return p.PeekTypeN(0, typ)
}
// Returns the CURRENT token if the given type AND value matches.
// It DOES NOT consume the token.
func (p *Parser) Peek(typ TokenType, val string) *Token {
return p.PeekN(0, typ, val)
}
// Returns the CURRENT token if the given type AND *one* of
// the given values matches.
// It DOES NOT consume the token.
func (p *Parser) PeekOne(typ TokenType, vals ...string) *Token {
for _, v := range vals {
t := p.PeekN(0, typ, v)
if t != nil {
return t
}
}
return nil
}
// Returns the tokens[current position + shift] token if the
// given type AND value matches for that token.
// DOES NOT consume the token.
func (p *Parser) PeekN(shift int, typ TokenType, val string) *Token {
t := p.Get(p.idx + shift)
if t != nil {
if t.Typ == typ && t.Val == val {
return t
}
}
return nil
}
// Returns the tokens[current position + shift] token if the given type matches.
// DOES NOT consume the token for that token.
func (p *Parser) PeekTypeN(shift int, typ TokenType) *Token {
t := p.Get(p.idx + shift)
if t != nil {
if t.Typ == typ {
return t
}
}
return nil
}
// Returns the UNCONSUMED token count.
func (p *Parser) Remaining() int {
return len(p.tokens) - p.idx
}
// Returns the total token count.
func (p *Parser) Count() int {
return len(p.tokens)
}
// Returns tokens[i] or NIL (if i >= len(tokens))
func (p *Parser) Get(i int) *Token {
if i < len(p.tokens) && i >= 0 {
return p.tokens[i]
}
return nil
}
// Returns tokens[current-position + shift] or NIL
// (if (current-position + i) >= len(tokens))
func (p *Parser) GetR(shift int) *Token {
i := p.idx + shift
return p.Get(i)
}
// Error produces a nice error message and returns an error-object.
// The 'token'-argument is optional. If provided, it will take
// the token's position information. If not provided, it will
// automatically use the CURRENT token's position information.
func (p *Parser) Error(msg string, token *Token) *Error {
if token == nil {
// Set current token
token = p.Current()
if token == nil {
// Set to last token
if len(p.tokens) > 0 {
token = p.tokens[len(p.tokens)-1]
}
}
}
var line, col int
if token != nil {
line = token.Line
col = token.Col
}
return &Error{
Template: p.template,
Filename: p.name,
Sender: "parser",
Line: line,
Column: col,
Token: token,
OrigError: errors.New(msg),
}
}
// Wraps all nodes between starting tag and "{% endtag %}" and provides
// one simple interface to execute the wrapped nodes.
// It returns a parser to process provided arguments to the tag.
func (p *Parser) WrapUntilTag(names ...string) (*NodeWrapper, *Parser, *Error) {
wrapper := &NodeWrapper{}
var tagArgs []*Token
for p.Remaining() > 0 {
// New tag, check whether we have to stop wrapping here
if p.Peek(TokenSymbol, "{%") != nil {
tagIdent := p.PeekTypeN(1, TokenIdentifier)
if tagIdent != nil {
// We've found a (!) end-tag
found := false
for _, n := range names {
if tagIdent.Val == n {
found = true
break
}
}
// We only process the tag if we've found an end tag
if found {
// Okay, endtag found.
p.ConsumeN(2) // '{%' tagname
for {
if p.Match(TokenSymbol, "%}") != nil {
// Okay, end the wrapping here
wrapper.Endtag = tagIdent.Val
return wrapper, newParser(p.template.name, tagArgs, p.template), nil
}
t := p.Current()
p.Consume()
if t == nil {
return nil, nil, p.Error("Unexpected EOF.", p.lastToken)
}
tagArgs = append(tagArgs, t)
}
}
}
}
// Otherwise process next element to be wrapped
node, err := p.parseDocElement()
if err != nil {
return nil, nil, err
}
wrapper.nodes = append(wrapper.nodes, node)
}
return nil, nil, p.Error(fmt.Sprintf("Unexpected EOF, expected tag %s.", strings.Join(names, " or ")),
p.lastToken)
}
// Skips all nodes between starting tag and "{% endtag %}"
func (p *Parser) SkipUntilTag(names ...string) *Error {
for p.Remaining() > 0 {
// New tag, check whether we have to stop wrapping here
if p.Peek(TokenSymbol, "{%") != nil {
tagIdent := p.PeekTypeN(1, TokenIdentifier)
if tagIdent != nil {
// We've found a (!) end-tag
found := false
for _, n := range names {
if tagIdent.Val == n {
found = true
break
}
}
// We only process the tag if we've found an end tag
if found {
// Okay, endtag found.
p.ConsumeN(2) // '{%' tagname
for {
if p.Match(TokenSymbol, "%}") != nil {
// Done skipping, exit.
return nil
}
}
}
}
}
t := p.Current()
p.Consume()
if t == nil {
return p.Error("Unexpected EOF.", p.lastToken)
}
}
return p.Error(fmt.Sprintf("Unexpected EOF, expected tag %s.", strings.Join(names, " or ")), p.lastToken)
}
package pongo2
// Doc = { ( Filter | Tag | HTML ) }
func (p *Parser) parseDocElement() (INode, *Error) {
t := p.Current()
switch t.Typ {
case TokenHTML:
n := &nodeHTML{token: t}
left := p.PeekTypeN(-1, TokenSymbol)
right := p.PeekTypeN(1, TokenSymbol)
n.trimLeft = left != nil && left.TrimWhitespaces
n.trimRight = right != nil && right.TrimWhitespaces
p.Consume() // consume HTML element
return n, nil
case TokenSymbol:
switch t.Val {
case "{{":
// parse variable
variable, err := p.parseVariableElement()
if err != nil {
return nil, err
}
return variable, nil
case "{%":
// parse tag
tag, err := p.parseTagElement()
if err != nil {
return nil, err
}
return tag, nil
}
}
return nil, p.Error("Unexpected token (only HTML/tags/filters in templates allowed)", t)
}
func (tpl *Template) parse() *Error {
tpl.parser = newParser(tpl.name, tpl.tokens, tpl)
doc, err := tpl.parser.parseDocument()
if err != nil {
return err
}
tpl.root = doc
return nil
}
func (p *Parser) parseDocument() (*nodeDocument, *Error) {
doc := &nodeDocument{}
for p.Remaining() > 0 {
node, err := p.parseDocElement()
if err != nil {
return nil, err
}
doc.Nodes = append(doc.Nodes, node)
}
return doc, nil
}
package pongo2
// Version string
const Version = "dev"
// Must panics, if a Template couldn't successfully parsed. This is how you
// would use it:
// var baseTemplate = pongo2.Must(pongo2.FromFile("templates/base.html"))
func Must(tpl *Template, err error) *Template {
if err != nil {
panic(err)
}
return tpl
}
package pongo2
/* Incomplete:
-----------
verbatim (only the "name" argument is missing for verbatim)
Reconsideration:
----------------
debug (reason: not sure what to output yet)
regroup / Grouping on other properties (reason: maybe too python-specific; not sure how useful this would be in Go)
Following built-in tags wont be added:
--------------------------------------
csrf_token (reason: web-framework specific)
load (reason: python-specific)
url (reason: web-framework specific)
*/
import (
"fmt"
"github.com/juju/errors"
)
type INodeTag interface {
INode
}
// This is the function signature of the tag's parser you will have
// to implement in order to create a new tag.
//
// 'doc' is providing access to the whole document while 'arguments'
// is providing access to the user's arguments to the tag:
//
// {% your_tag_name some "arguments" 123 %}
//
// start_token will be the *Token with the tag's name in it (here: your_tag_name).
//
// Please see the Parser documentation on how to use the parser.
// See RegisterTag()'s documentation for more information about
// writing a tag as well.
type TagParser func(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error)
type tag struct {
name string
parser TagParser
}
var tags map[string]*tag
func init() {
tags = make(map[string]*tag)
}
// Registers a new tag. You usually want to call this
// function in the tag's init() function:
// http://golang.org/doc/effective_go.html#init
//
// See http://www.florian-schlachter.de/post/pongo2/ for more about
// writing filters and tags.
func RegisterTag(name string, parserFn TagParser) error {
_, existing := tags[name]
if existing {
return errors.Errorf("tag with name '%s' is already registered", name)
}
tags[name] = &tag{
name: name,
parser: parserFn,
}
return nil
}
// Replaces an already registered tag with a new implementation. Use this
// function with caution since it allows you to change existing tag behaviour.
func ReplaceTag(name string, parserFn TagParser) error {
_, existing := tags[name]
if !existing {
return errors.Errorf("tag with name '%s' does not exist (therefore cannot be overridden)", name)
}
tags[name] = &tag{
name: name,
parser: parserFn,
}
return nil
}
// Tag = "{%" IDENT ARGS "%}"
func (p *Parser) parseTagElement() (INodeTag, *Error) {
p.Consume() // consume "{%"
tokenName := p.MatchType(TokenIdentifier)
// Check for identifier
if tokenName == nil {
return nil, p.Error("Tag name must be an identifier.", nil)
}
// Check for the existing tag
tag, exists := tags[tokenName.Val]
if !exists {
// Does not exists
return nil, p.Error(fmt.Sprintf("Tag '%s' not found (or beginning tag not provided)", tokenName.Val), tokenName)
}
// Check sandbox tag restriction
if _, isBanned := p.template.set.bannedTags[tokenName.Val]; isBanned {
return nil, p.Error(fmt.Sprintf("Usage of tag '%s' is not allowed (sandbox restriction active).", tokenName.Val), tokenName)
}
var argsToken []*Token
for p.Peek(TokenSymbol, "%}") == nil && p.Remaining() > 0 {
// Add token to args
argsToken = append(argsToken, p.Current())
p.Consume() // next token
}
// EOF?
if p.Remaining() == 0 {
return nil, p.Error("Unexpectedly reached EOF, no tag end found.", p.lastToken)
}
p.Match(TokenSymbol, "%}")
argParser := newParser(p.name, argsToken, p.template)
if len(argsToken) == 0 {
// This is done to have nice EOF error messages
argParser.lastToken = tokenName
}
p.template.level++
defer func() { p.template.level-- }()
return tag.parser(p, tokenName, argParser)
}
package pongo2
type tagAutoescapeNode struct {
wrapper *NodeWrapper
autoescape bool
}
func (node *tagAutoescapeNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
old := ctx.Autoescape
ctx.Autoescape = node.autoescape
err := node.wrapper.Execute(ctx, writer)
if err != nil {
return err
}
ctx.Autoescape = old
return nil
}
func tagAutoescapeParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
autoescapeNode := &tagAutoescapeNode{}
wrapper, _, err := doc.WrapUntilTag("endautoescape")
if err != nil {
return nil, err
}
autoescapeNode.wrapper = wrapper
modeToken := arguments.MatchType(TokenIdentifier)
if modeToken == nil {
return nil, arguments.Error("A mode is required for autoescape-tag.", nil)
}
if modeToken.Val == "on" {
autoescapeNode.autoescape = true
} else if modeToken.Val == "off" {
autoescapeNode.autoescape = false
} else {
return nil, arguments.Error("Only 'on' or 'off' is valid as an autoescape-mode.", nil)
}
if arguments.Remaining() > 0 {
return nil, arguments.Error("Malformed autoescape-tag arguments.", nil)
}
return autoescapeNode, nil
}
func init() {
RegisterTag("autoescape", tagAutoescapeParser)
}
package pongo2
import (
"bytes"
"fmt"
)
type tagBlockNode struct {
name string
}
func (node *tagBlockNode) getBlockWrappers(tpl *Template) []*NodeWrapper {
nodeWrappers := make([]*NodeWrapper, 0)
var t *NodeWrapper
for tpl != nil {
t = tpl.blocks[node.name]
if t != nil {
nodeWrappers = append(nodeWrappers, t)
}
tpl = tpl.child
}
return nodeWrappers
}
func (node *tagBlockNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
tpl := ctx.template
if tpl == nil {
panic("internal error: tpl == nil")
}
// Determine the block to execute
blockWrappers := node.getBlockWrappers(tpl)
lenBlockWrappers := len(blockWrappers)
if lenBlockWrappers == 0 {
return ctx.Error("internal error: len(block_wrappers) == 0 in tagBlockNode.Execute()", nil)
}
blockWrapper := blockWrappers[lenBlockWrappers-1]
ctx.Private["block"] = tagBlockInformation{
ctx: ctx,
wrappers: blockWrappers[0 : lenBlockWrappers-1],
}
err := blockWrapper.Execute(ctx, writer)
if err != nil {
return err
}
return nil
}
type tagBlockInformation struct {
ctx *ExecutionContext
wrappers []*NodeWrapper
}
func (t tagBlockInformation) Super() string {
lenWrappers := len(t.wrappers)
if lenWrappers == 0 {
return ""
}
superCtx := NewChildExecutionContext(t.ctx)
superCtx.Private["block"] = tagBlockInformation{
ctx: t.ctx,
wrappers: t.wrappers[0 : lenWrappers-1],
}
blockWrapper := t.wrappers[lenWrappers-1]
buf := bytes.NewBufferString("")
err := blockWrapper.Execute(superCtx, &templateWriter{buf})
if err != nil {
return ""
}
return buf.String()
}
func tagBlockParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
if arguments.Count() == 0 {
return nil, arguments.Error("Tag 'block' requires an identifier.", nil)
}
nameToken := arguments.MatchType(TokenIdentifier)
if nameToken == nil {
return nil, arguments.Error("First argument for tag 'block' must be an identifier.", nil)
}
if arguments.Remaining() != 0 {
return nil, arguments.Error("Tag 'block' takes exactly 1 argument (an identifier).", nil)
}
wrapper, endtagargs, err := doc.WrapUntilTag("endblock")
if err != nil {
return nil, err
}
if endtagargs.Remaining() > 0 {
endtagnameToken := endtagargs.MatchType(TokenIdentifier)
if endtagnameToken != nil {
if endtagnameToken.Val != nameToken.Val {
return nil, endtagargs.Error(fmt.Sprintf("Name for 'endblock' must equal to 'block'-tag's name ('%s' != '%s').",
nameToken.Val, endtagnameToken.Val), nil)
}
}
if endtagnameToken == nil || endtagargs.Remaining() > 0 {
return nil, endtagargs.Error("Either no or only one argument (identifier) allowed for 'endblock'.", nil)
}
}
tpl := doc.template
if tpl == nil {
panic("internal error: tpl == nil")
}
_, hasBlock := tpl.blocks[nameToken.Val]
if !hasBlock {
tpl.blocks[nameToken.Val] = wrapper
} else {
return nil, arguments.Error(fmt.Sprintf("Block named '%s' already defined", nameToken.Val), nil)
}
return &tagBlockNode{name: nameToken.Val}, nil
}
func init() {
RegisterTag("block", tagBlockParser)
}
package pongo2
type tagCommentNode struct{}
func (node *tagCommentNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
return nil
}
func tagCommentParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
commentNode := &tagCommentNode{}
// TODO: Process the endtag's arguments (see django 'comment'-tag documentation)
err := doc.SkipUntilTag("endcomment")
if err != nil {
return nil, err
}
if arguments.Count() != 0 {
return nil, arguments.Error("Tag 'comment' does not take any argument.", nil)
}
return commentNode, nil
}
func init() {
RegisterTag("comment", tagCommentParser)
}
package pongo2
type tagCycleValue struct {
node *tagCycleNode
value *Value
}
type tagCycleNode struct {
position *Token
args []IEvaluator
idx int
asName string
silent bool
}
func (cv *tagCycleValue) String() string {
return cv.value.String()
}
func (node *tagCycleNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
item := node.args[node.idx%len(node.args)]
node.idx++
val, err := item.Evaluate(ctx)
if err != nil {
return err
}
if t, ok := val.Interface().(*tagCycleValue); ok {
// {% cycle "test1" "test2"
// {% cycle cycleitem %}
// Update the cycle value with next value
item := t.node.args[t.node.idx%len(t.node.args)]
t.node.idx++
val, err := item.Evaluate(ctx)
if err != nil {
return err
}
t.value = val
if !t.node.silent {
writer.WriteString(val.String())
}
} else {
// Regular call
cycleValue := &tagCycleValue{
node: node,
value: val,
}
if node.asName != "" {
ctx.Private[node.asName] = cycleValue
}
if !node.silent {
writer.WriteString(val.String())
}
}
return nil
}
// HINT: We're not supporting the old comma-separated list of expressions argument-style
func tagCycleParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
cycleNode := &tagCycleNode{
position: start,
}
for arguments.Remaining() > 0 {
node, err := arguments.ParseExpression()
if err != nil {
return nil, err
}
cycleNode.args = append(cycleNode.args, node)
if arguments.MatchOne(TokenKeyword, "as") != nil {
// as
nameToken := arguments.MatchType(TokenIdentifier)
if nameToken == nil {
return nil, arguments.Error("Name (identifier) expected after 'as'.", nil)
}
cycleNode.asName = nameToken.Val
if arguments.MatchOne(TokenIdentifier, "silent") != nil {
cycleNode.silent = true
}
// Now we're finished
break
}
}
if arguments.Remaining() > 0 {
return nil, arguments.Error("Malformed cycle-tag.", nil)
}
return cycleNode, nil
}
func init() {
RegisterTag("cycle", tagCycleParser)
}
package pongo2
type tagExtendsNode struct {
filename string
}
func (node *tagExtendsNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
return nil
}
func tagExtendsParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
extendsNode := &tagExtendsNode{}
if doc.template.level > 1 {
return nil, arguments.Error("The 'extends' tag can only defined on root level.", start)
}
if doc.template.parent != nil {
// Already one parent
return nil, arguments.Error("This template has already one parent.", start)
}
if filenameToken := arguments.MatchType(TokenString); filenameToken != nil {
// prepared, static template
// Get parent's filename
parentFilename := doc.template.set.resolveFilename(doc.template, filenameToken.Val)
// Parse the parent
parentTemplate, err := doc.template.set.FromFile(parentFilename)
if err != nil {
return nil, err.(*Error)
}
// Keep track of things
parentTemplate.child = doc.template
doc.template.parent = parentTemplate
extendsNode.filename = parentFilename
} else {
return nil, arguments.Error("Tag 'extends' requires a template filename as string.", nil)
}
if arguments.Remaining() > 0 {
return nil, arguments.Error("Tag 'extends' does only take 1 argument.", nil)
}
return extendsNode, nil
}
func init() {
RegisterTag("extends", tagExtendsParser)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment