Merge pull request #194 from gabriel-samfira/remove-lxd-provider

Remove the LXD internal provider
This commit is contained in:
Gabriel 2023-12-18 18:15:54 +02:00 committed by GitHub
commit a13c5db1a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
519 changed files with 95 additions and 95202 deletions

View file

@ -10,6 +10,9 @@ ADD . /build/garm
RUN cd /build/garm && git checkout ${GARM_REF}
RUN git clone https://github.com/cloudbase/garm-provider-azure /build/garm-provider-azure
RUN git clone https://github.com/cloudbase/garm-provider-openstack /build/garm-provider-openstack
RUN git clone https://github.com/cloudbase/garm-provider-lxd /build/garm-provider-lxd
RUN git clone https://github.com/cloudbase/garm-provider-incus /build/garm-provider-incus
RUN git clone https://github.com/mercedes-benz/garm-provider-k8s /build/garm-provider-k8s
RUN cd /build/garm && go build -o /bin/garm \
-tags osusergo,netgo,sqlite_omit_load_extension \
@ -18,11 +21,17 @@ RUN cd /build/garm && go build -o /bin/garm \
RUN mkdir -p /opt/garm/providers.d
RUN cd /build/garm-provider-azure && go build -ldflags="-extldflags '-static' -s -w" -o /opt/garm/providers.d/garm-provider-azure .
RUN cd /build/garm-provider-openstack && go build -ldflags="-extldflags '-static' -s -w" -o /opt/garm/providers.d/garm-provider-openstack .
RUN cd /build/garm-provider-lxd && go build -ldflags="-extldflags '-static' -s -w" -o /opt/garm/providers.d/garm-provider-lxd .
RUN cd /build/garm-provider-incus && go build -ldflags="-extldflags '-static' -s -w" -o /opt/garm/providers.d/garm-provider-incus .
RUN cd /build/garm-provider-k8s/cmd/garm-provider-k8s && go build -ldflags="-extldflags '-static' -s -w" -o /opt/garm/providers.d/garm-provider-k8s .
FROM scratch
COPY --from=builder /bin/garm /bin/garm
COPY --from=builder /opt/garm/providers.d/garm-provider-openstack /opt/garm/providers.d/garm-provider-openstack
COPY --from=builder /opt/garm/providers.d/garm-provider-lxd /opt/garm/providers.d/garm-provider-lxd
COPY --from=builder /opt/garm/providers.d/garm-provider-incus /opt/garm/providers.d/garm-provider-incus
COPY --from=builder /opt/garm/providers.d/garm-provider-k8s /opt/garm/providers.d/garm-provider-k8s
COPY --from=builder /opt/garm/providers.d/garm-provider-azure /opt/garm/providers.d/garm-provider-azure
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

View file

@ -30,17 +30,17 @@ Thanks to the efforts of the amazing folks at @mercedes-benz, GARM can now be in
## Supported providers
GARM has a built-in LXD provider that you can use out of the box to spin up runners on any machine that runs either a stand-alone LXD instance, or an LXD cluster. The quick start guide mentioned above will get you up and running with the LXD provider.
GARM also supports external providers for a variety of other targets.
GARM uses providers to create runners in a particular IaaS. The providers are external executables that GARM calls into to create runners. Before you can create runners, you'll need to install at least one provider.
## Installing external providers
External providers are binaries that GARM calls into to create runners in a particular IaaS. There are currently two external providers available:
External providers are binaries that GARM calls into to create runners in a particular IaaS. There are several external providers available:
* [OpenStack](https://github.com/cloudbase/garm-provider-openstack)
* [Azure](https://github.com/cloudbase/garm-provider-azure)
* [Kubernetes](https://github.com/mercedes-benz/garm-provider-k8s) - Thanks to the amazing folks at @mercedes-benz for sharing their awesome provider!
* [LXD](https://github.com/cloudbase/garm-provider-lxd)
* [Incus](https://github.com/cloudbase/garm-provider-incus)
Follow the instructions in the README of each provider to install them.
@ -62,10 +62,4 @@ If you would like to optimize the startup time of new instance, take a look at t
## Write your own provider
The providers are interfaces between ```GARM``` and a particular IaaS in which we spin up GitHub Runners. These providers can be either **native** or **external**. The **native** providers are written in ```Go```, and must implement [the interface defined here](https://github.com/cloudbase/garm/blob/main/runner/common/provider.go#L22-L39). **External** providers can be written in any language, as they are in the form of an external executable that ```GARM``` calls into.
There is currently one **native** provider for [LXD](https://linuxcontainers.org/lxd/) and several **external** providers linked above.
If you want to write your own provider, you can choose to write a native one, or implement an **external** one. I encourage you to opt for an **external** provider, as those are the easiest to write and you don't need to merge it in GARM itself to be able to use. Faster to write, faster to iterate. The LXD provider may at some point be split from GARM into it's own external project, at which point we will remove the native provider interface and only support external providers.
Please see the [Writing an external provider](/doc/external_provider.md) document for details. Also, feel free to inspect the two available sample external providers in this repository.
The providers are interfaces between ```GARM``` and a particular IaaS in which we spin up GitHub Runners. **External** providers can be written in any language, as they are in the form of an external executable that ```GARM``` calls into. Please see the [Writing an external provider](/doc/external_provider.md) document for details. Also, feel free to inspect the two available sample external providers in this repository.

View file

@ -223,7 +223,6 @@ type Provider struct {
// tokens to be used. This may happen if a provider has not yet been updated to support
// JIT configuration.
DisableJITConfig bool `toml:"disable_jit_config" json:"disable-jit-config"`
LXD LXD `toml:"lxd" json:"lxd"`
External External `toml:"external" json:"external"`
}
@ -233,10 +232,6 @@ func (p *Provider) Validate() error {
}
switch p.ProviderType {
case params.LXDProvider:
if err := p.LXD.Validate(); err != nil {
return errors.Wrap(err, "validating LXD provider info")
}
case params.ExternalProvider:
if err := p.External.Validate(); err != nil {
return errors.Wrap(err, "validating external provider info")

View file

@ -20,7 +20,6 @@ import (
"testing"
"time"
"github.com/cloudbase/garm/params"
"github.com/cloudbase/garm/util/appdefaults"
"github.com/stretchr/testify/require"
)
@ -76,15 +75,7 @@ func getDefaultDatabaseConfig(dir string) Database {
}
func getDefaultProvidersConfig() []Provider {
lxdConfig := getDefaultLXDConfig()
return []Provider{
{
Name: "test_lxd",
ProviderType: params.LXDProvider,
Description: "test LXD provider",
LXD: lxdConfig,
},
}
return []Provider{}
}
func getDefaultGithubConfig() []Github {

View file

@ -1,163 +0,0 @@
// Copyright 2022 Cloudbase Solutions SRL
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package config
import (
"fmt"
"net/url"
"os"
"github.com/pkg/errors"
)
type LXDRemoteProtocol string
type LXDImageType string
func (l LXDImageType) String() string {
return string(l)
}
const (
SimpleStreams LXDRemoteProtocol = "simplestreams"
LXDImageVirtualMachine LXDImageType = "virtual-machine"
LXDImageContainer LXDImageType = "container"
)
// LXDImageRemote holds information about a remote server from which LXD can fetch
// OS images. Typically this will be a simplestreams server.
type LXDImageRemote struct {
Address string `toml:"addr" json:"addr"`
Public bool `toml:"public" json:"public"`
Protocol LXDRemoteProtocol `toml:"protocol" json:"protocol"`
InsecureSkipVerify bool `toml:"skip_verify" json:"skip-verify"`
}
func (l *LXDImageRemote) Validate() error {
if l.Protocol != SimpleStreams {
// Only supports simplestreams for now.
return fmt.Errorf("invalid remote protocol %s. Supported protocols: %s", l.Protocol, SimpleStreams)
}
if l.Address == "" {
return fmt.Errorf("missing address")
}
url, err := url.ParseRequestURI(l.Address)
if err != nil {
return errors.Wrap(err, "validating address")
}
if url.Scheme != "http" && url.Scheme != "https" {
return fmt.Errorf("address must be http or https")
}
return nil
}
// LXD holds connection information for an LXD cluster.
type LXD struct {
// UnixSocket is the path on disk to the LXD unix socket. If defined,
// this is prefered over connecting via HTTPs.
UnixSocket string `toml:"unix_socket_path" json:"unix-socket-path"`
// Project name is the name of the project in which this runner will create
// instances. If this option is not set, the default project will be used.
// The project used here, must have all required profiles created by you
// beforehand. For LXD, the "flavor" used in the runner definition for a pool
// equates to a profile in the desired project.
ProjectName string `toml:"project_name" json:"project-name"`
// IncludeDefaultProfile specifies whether or not this provider will always add
// the "default" profile to any newly created instance.
IncludeDefaultProfile bool `toml:"include_default_profile" json:"include-default-profile"`
// URL holds the URL of the remote LXD server.
// example: https://10.10.10.1:8443/
URL string `toml:"url" json:"url"`
// ClientCertificate is the x509 client certificate path used for authentication.
ClientCertificate string `toml:"client_certificate" json:"client_certificate"`
// ClientKey is the key used for client certificate authentication.
ClientKey string `toml:"client_key" json:"client-key"`
// TLS certificate of the remote server. If not specified, the system CA is used.
TLSServerCert string `toml:"tls_server_certificate" json:"tls-server-certificate"`
// TLSCA is the TLS CA certificate when running LXD in PKI mode.
TLSCA string `toml:"tls_ca" json:"tls-ca"`
// ImageRemotes is a map to a set of remote image repositories we can use to
// download images.
ImageRemotes map[string]LXDImageRemote `toml:"image_remotes" json:"image-remotes"`
// SecureBoot enables secure boot for VMs spun up using this provider.
SecureBoot bool `toml:"secure_boot" json:"secure-boot"`
// InstanceType allows you to choose between a virtual machine and a container
InstanceType LXDImageType `toml:"instance_type" json:"instance-type"`
}
func (l *LXD) GetInstanceType() LXDImageType {
switch l.InstanceType {
case LXDImageVirtualMachine, LXDImageContainer:
return l.InstanceType
default:
return LXDImageVirtualMachine
}
}
func (l *LXD) Validate() error {
if l.UnixSocket != "" {
if _, err := os.Stat(l.UnixSocket); err != nil {
return fmt.Errorf("could not access unix socket %s: %q", l.UnixSocket, err)
}
return nil
}
if l.URL == "" {
return fmt.Errorf("unix_socket or address must be specified")
}
url, err := url.ParseRequestURI(l.URL)
if err != nil {
return fmt.Errorf("invalid LXD URL")
}
if url.Scheme != "https" {
return fmt.Errorf("address must be https")
}
if l.ClientCertificate == "" || l.ClientKey == "" {
return fmt.Errorf("client_certificate and client_key are mandatory")
}
if _, err := os.Stat(l.ClientCertificate); err != nil {
return fmt.Errorf("failed to access client certificate %s: %q", l.ClientCertificate, err)
}
if _, err := os.Stat(l.ClientKey); err != nil {
return fmt.Errorf("failed to access client key %s: %q", l.ClientKey, err)
}
if l.TLSServerCert != "" {
if _, err := os.Stat(l.TLSServerCert); err != nil {
return fmt.Errorf("failed to access tls_server_certificate %s: %q", l.TLSServerCert, err)
}
}
for name, val := range l.ImageRemotes {
if err := val.Validate(); err != nil {
return fmt.Errorf("remote %s is invalid: %s", name, err)
}
}
return nil
}

View file

@ -1,175 +0,0 @@
// Copyright 2022 Cloudbase Solutions SRL
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package config
import (
"testing"
"github.com/stretchr/testify/require"
)
func getDefaultLXDImageRemoteConfig() LXDImageRemote {
return LXDImageRemote{
Address: "https://cloud-images.ubuntu.com/releases",
Public: true,
Protocol: SimpleStreams,
InsecureSkipVerify: false,
}
}
func getDefaultLXDConfig() LXD {
remote := getDefaultLXDImageRemoteConfig()
return LXD{
URL: "https://example.com:8443",
ProjectName: "default",
IncludeDefaultProfile: false,
ClientCertificate: "../testdata/lxd/certs/client.crt",
ClientKey: "../testdata/lxd/certs/client.key",
TLSServerCert: "../testdata/lxd/certs/servercert.crt",
ImageRemotes: map[string]LXDImageRemote{
"default": remote,
},
SecureBoot: false,
}
}
func TestLXDRemote(t *testing.T) {
cfg := getDefaultLXDImageRemoteConfig()
err := cfg.Validate()
require.Nil(t, err)
}
func TestLXDRemoteEmptyAddress(t *testing.T) {
cfg := getDefaultLXDImageRemoteConfig()
cfg.Address = ""
err := cfg.Validate()
require.NotNil(t, err)
require.EqualError(t, err, "missing address")
}
func TestLXDRemoteInvalidAddress(t *testing.T) {
cfg := getDefaultLXDImageRemoteConfig()
cfg.Address = "bogus address"
err := cfg.Validate()
require.NotNil(t, err)
require.EqualError(t, err, "validating address: parse \"bogus address\": invalid URI for request")
}
func TestLXDRemoteIvalidAddressScheme(t *testing.T) {
cfg := getDefaultLXDImageRemoteConfig()
cfg.Address = "ftp://whatever"
err := cfg.Validate()
require.NotNil(t, err)
require.EqualError(t, err, "address must be http or https")
}
func TestLXDConfig(t *testing.T) {
cfg := getDefaultLXDConfig()
err := cfg.Validate()
require.Nil(t, err)
}
func TestLXDWithInvalidUnixSocket(t *testing.T) {
cfg := getDefaultLXDConfig()
cfg.UnixSocket = "bogus unix socket"
err := cfg.Validate()
require.NotNil(t, err)
require.EqualError(t, err, "could not access unix socket bogus unix socket: \"stat bogus unix socket: no such file or directory\"")
}
func TestMissingUnixSocketAndMissingURL(t *testing.T) {
cfg := getDefaultLXDConfig()
cfg.URL = ""
cfg.UnixSocket = ""
err := cfg.Validate()
require.NotNil(t, err)
require.EqualError(t, err, "unix_socket or address must be specified")
}
func TestInvalidLXDURL(t *testing.T) {
cfg := getDefaultLXDConfig()
cfg.URL = "bogus"
err := cfg.Validate()
require.NotNil(t, err)
require.EqualError(t, err, "invalid LXD URL")
}
func TestLXDURLIsHTTPS(t *testing.T) {
cfg := getDefaultLXDConfig()
cfg.URL = "http://example.com"
err := cfg.Validate()
require.NotNil(t, err)
require.EqualError(t, err, "address must be https")
}
func TestMissingClientCertOrKey(t *testing.T) {
cfg := getDefaultLXDConfig()
cfg.ClientKey = ""
err := cfg.Validate()
require.NotNil(t, err)
require.EqualError(t, err, "client_certificate and client_key are mandatory")
cfg = getDefaultLXDConfig()
cfg.ClientCertificate = ""
err = cfg.Validate()
require.NotNil(t, err)
require.EqualError(t, err, "client_certificate and client_key are mandatory")
}
func TestLXDIvalidCertOrKeyPaths(t *testing.T) {
cfg := getDefaultLXDConfig()
cfg.ClientCertificate = "/i/am/not/here"
err := cfg.Validate()
require.NotNil(t, err)
require.EqualError(t, err, "failed to access client certificate /i/am/not/here: \"stat /i/am/not/here: no such file or directory\"")
cfg.ClientCertificate = "../testdata/lxd/certs/client.crt"
cfg.ClientKey = "/me/neither"
err = cfg.Validate()
require.NotNil(t, err)
require.EqualError(t, err, "failed to access client key /me/neither: \"stat /me/neither: no such file or directory\"")
}
func TestLXDInvalidServerCertPath(t *testing.T) {
cfg := getDefaultLXDConfig()
cfg.TLSServerCert = "/not/a/valid/server/cert/path"
err := cfg.Validate()
require.NotNil(t, err)
require.EqualError(t, err, "failed to access tls_server_certificate /not/a/valid/server/cert/path: \"stat /not/a/valid/server/cert/path: no such file or directory\"")
}
func TestInvalidLXDImageRemotes(t *testing.T) {
cfg := getDefaultLXDConfig()
cfg.ImageRemotes["default"] = LXDImageRemote{
Protocol: LXDRemoteProtocol("bogus"),
}
err := cfg.Validate()
require.NotNil(t, err)
require.EqualError(t, err, "remote default is invalid: invalid remote protocol bogus. Supported protocols: simplestreams")
}

View file

@ -1,126 +1,10 @@
# Provider configuration
GARM was designed to be extensible. Providers can be written either as built-in plugins or as external executables. The built-in plugins are written in Go, and they are compiled into the ```GARM``` binary. External providers are executables that implement the needed interface to create/delete/list compute systems that are used by ```GARM``` to create runners.
GARM was designed to be extensible. Providers can be written as external executables. External providers are executables that implement the needed interface to create/delete/list compute systems that are used by ```GARM``` to create runners.
GARM currently ships with one built-in provider for [LXD](https://linuxcontainers.org/lxd/introduction/) and the external provider interface which allows you to write your own provider in any language you want.
- [LXD provider](#lxd-provider)
- [LXD remotes](#lxd-remotes)
- [LXD Security considerations](#lxd-security-considerations)
- [External provider](#external-provider)
- [Available external providers](#available-external-providers)
## LXD provider
GARM leverages LXD to create the runners. Here is a sample config section for an LXD provider:
```toml
# Currently, providers are defined statically in the config. This is due to the fact
# that we have not yet added support for storing secrets in something like Barbican
# or Vault. This will change in the future. However, for now, it's important to remember
# that once you create a pool using one of the providers defined here, the name of that
# provider must not be changed, or the pool will no longer work. Make sure you remove any
# pools before removing or changing a provider.
[[provider]]
# An arbitrary string describing this provider.
name = "lxd_local"
# Provider type. GARM is designed to allow creating providers which are used to spin
# up compute resources, which in turn will run the github runner software.
# Currently, LXD is the only supprted provider, but more will be written in the future.
provider_type = "lxd"
# A short description of this provider. The name, description and provider types will
# be included in the information returned by the API when listing available providers.
description = "Local LXD installation"
[provider.lxd]
# the path to the unix socket that LXD is listening on. This works if GARM and LXD
# are on the same system, and this option takes precedence over the "url" option,
# which connects over the network.
unix_socket_path = "/var/snap/lxd/common/lxd/unix.socket"
# When defining a pool for a repository or an organization, you have an option to
# specify a "flavor". In LXD terms, this translates to "profiles". Profiles allow
# you to customize your instances (memory, cpu, disks, nics, etc).
# This option allows you to inject the "default" profile along with the profile selected
# by the flavor.
include_default_profile = false
# instance_type defines the type of instances this provider will create.
#
# Options are:
#
# * virtual-machine (default)
# * container
#
instance_type = "container"
# enable/disable secure boot. If the image you select for the pool does not have a
# signed bootloader, set this to false, otherwise your instances won't boot.
secure_boot = false
# Project name to use. You can create a separate project in LXD for runners.
project_name = "default"
# URL is the address on which LXD listens for connections (ex: https://example.com:8443)
url = ""
# GARM supports certificate authentication for LXD remote connections. The easiest way
# to get the needed certificates, is to install the lxc client and add a remote. The
# client_certificate, client_key and tls_server_certificate can be then fetched from
# $HOME/snap/lxd/common/config.
client_certificate = ""
client_key = ""
tls_server_certificate = ""
[provider.lxd.image_remotes]
# Image remotes are important. These are the default remotes used by lxc. The names
# of these remotes are important. When specifying an "image" for the pool, that image
# can be a hash of an existing image on your local LXD installation or it can be a
# remote image from one of these remotes. You can specify the images as follows:
# Example:
#
# * ubuntu:20.04
# * ubuntu_daily:20.04
# * images:centos/8/cloud
#
# Ubuntu images come pre-installed with cloud-init which we use to set up the runner
# automatically and customize the runner. For non Ubuntu images, you need to use the
# variant that has "/cloud" in the name. Those images come with cloud-init.
[provider.lxd.image_remotes.ubuntu]
addr = "https://cloud-images.ubuntu.com/releases"
public = true
protocol = "simplestreams"
skip_verify = false
[provider.lxd.image_remotes.ubuntu_daily]
addr = "https://cloud-images.ubuntu.com/daily"
public = true
protocol = "simplestreams"
skip_verify = false
[provider.lxd.image_remotes.images]
addr = "https://images.linuxcontainers.org"
public = true
protocol = "simplestreams"
skip_verify = false
```
You can choose to connect to a local LXD server by using the ```unix_socket_path``` option, or you can connect to a remote LXD cluster/server by using the ```url``` option. If both are specified, the unix socket takes precedence. The config file is fairly well commented, but I will add a note about remotes.
### LXD remotes
By default, GARM does not load any image remotes. You get to choose which remotes you add (if any). An image remote is a repository of images that LXD uses to create new instances, either virtual machines or containers. In the absence of any remote, GARM will attempt to find the image you configure for a pool of runners, on the LXD server we're connecting to. If one is present, it will be used, otherwise it will fail and you will need to configure a remote.
The sample config file in this repository has the usual default ```LXD``` remotes:
* <https://cloud-images.ubuntu.com/releases> (ubuntu) - Official Ubuntu images
* <https://cloud-images.ubuntu.com/daily> (ubuntu_daily) - Official Ubuntu images, daily build
* <https://images.linuxcontainers.org> (images) - Community maintained images for various operating systems
When creating a new pool, you'll be able to specify which image you want to use. The images are referenced by ```remote_name:image_tag```. For example, if you want to launch a runner on an Ubuntu 20.04, the image name would be ```ubuntu:20.04```. For a daily image it would be ```ubuntu_daily:20.04```. And for one of the unofficial images it would be ```images:centos/8-Stream/cloud```. Note, for unofficial images you need to use the tags that have ```/cloud``` in the name. These images come pre-installed with ```cloud-init``` which we need to set up the runners automatically.
You can also create your own image remote, where you can host your own custom images. If you want to build your own images, have a look at [distrobuilder](https://github.com/lxc/distrobuilder).
Image remotes in the ```GARM``` config, is a map of strings to remote settings. The name of the remote is the last bit of string in the section header. For example, the following section ```[provider.lxd.image_remotes.ubuntu_daily]```, defines the image remote named **ubuntu_daily**. Use this name to reference images inside that remote.
You can also use locally uploaded images. Check out the [performance considerations](./performance_considerations.md) page for details on how to customize local images and use them with GARM.
### LXD Security considerations
GARM does not apply any ACLs of any kind to the instances it creates. That task remains in the responsibility of the user. [Here is a guide for creating ACLs in LXD](https://linuxcontainers.org/lxd/docs/master/howto/network_acls/). You can of course use ```iptables``` or ```nftables``` to create any rules you wish. I recommend you create a separate isolated lxd bridge for runners, and secure it using ACLs/iptables/nftables.
You must make sure that the code that runs as part of the workflows is trusted, and if that cannot be done, you must make sure that any malicious code that will be pulled in by the actions and run as part of a workload, is as contained as possible. There is a nice article about [securing your workflow runs here](https://blog.gitguardian.com/github-actions-security-cheat-sheet/).
## External provider
The external provider is a special kind of provider. It delegates the functionality needed to create the runners to external executables. These executables can be either binaries or scripts. As long as they adhere to the needed interface, they can be used to create runners in any target IaaS. This is identical to what ```containerd``` does with ```CNIs```.
@ -163,6 +47,9 @@ For non testing purposes, there are two external providers currently available:
* [OpenStack](https://github.com/cloudbase/garm-provider-openstack)
* [Azure](https://github.com/cloudbase/garm-provider-azure)
* [Kubernetes](https://github.com/mercedes-benz/garm-provider-k8s) - Thanks to the amazing folks at @mercedes-benz for sharing their awesome provider!
* [LXD](https://github.com/cloudbase/garm-provider-lxd)
* [Incus](https://github.com/cloudbase/garm-provider-incus)
Details on how to install and configure them are available in their respective repositories.

View file

@ -96,23 +96,31 @@ At this point, we have a valid config file, but we still need to add `provider`
This is where you have a decision to make. GARM has a number of providers you can leverage. At the time of this writing, we have support for:
* LXD
* Azure
* OpenStack
* [OpenStack](https://github.com/cloudbase/garm-provider-openstack)
* [Azure](https://github.com/cloudbase/garm-provider-azure)
* [Kubernetes](https://github.com/mercedes-benz/garm-provider-k8s) - Thanks to the amazing folks at @mercedes-benz for sharing their awesome provider!
* [LXD](https://github.com/cloudbase/garm-provider-lxd)
* [Incus](https://github.com/cloudbase/garm-provider-incus)
The LXD provider is built into GARM itself and has no external requirements. The [Azure](https://github.com/cloudbase/garm-provider-azure) and [OpenStack](https://github.com/cloudbase/garm-provider-openstack) ones are `external` providers in the form of an executable that GARM calls into.
All currently available providers are `external`.
Both the LXD and the external provider configs are [documented in a separate doc](./providers.md).
The easiest provider to set up is probably the LXD provider. You don't need an account on an external cloud. You can just use your machine.
The easiest provider to set up is probably the LXD or Incus provider. Incus is a fork of LXD so the functionality is identical (for now). For the purpose of this document, we'll continue with LXD. You don't need an account on an external cloud. You can just use your machine.
You will need to have LXD installed and configured. There is an excellent [getting started guide](https://documentation.ubuntu.com/lxd/en/latest/getting_started/) for LXD. Follow the instructions there to install and configure LXD, then come back here.
Once you have LXD installed and configured, you can add the provider section to your config file. If you're connecting to the `local` LXD installation, the [config snippet for the LXD provider](./providers.md#lxd-provider) will work out of the box. We'll be connecting using the unix socket so no further configuration will be needed.
Once you have LXD installed and configured, you can add the provider section to your config file. If you're connecting to the `local` LXD installation, the [config snippet for the LXD provider](https://github.com/cloudbase/garm-provider-lxd/blob/main/testdata/garm-provider-lxd.toml) will work out of the box. We'll be connecting using the unix socket so no further configuration will be needed.
Go ahead and copy and paste that entire snippet in your GARM config file (`/etc/garm/config.toml`).
Go ahead and create a new config somwhere where GARM can access it and paste that entire snippet. For the purposes of this doc, we'll assume you created a new file called `/etc/garm/garm-provider-lxd.toml`. Now we need to define the external provider config in `/etc/garm/config.toml`:
You can also use an external provider instead of LXD. You will need to define the provider section in your config file and point it to the executable and the provider config file. The [config snippet for the external provider](./providers.md#external-provider) gives you an example of how that can be done. Configuring the external provider is outside the scope of this guide. You will need to consult the documentation for the external provider you want to use.
```toml
[[provider]]
name = "lxd_local"
provider_type = "external"
description = "Local LXD installation"
[provider.external]
provider_executable = "/opt/garm/providers.d/garm-provider-lxd"
config_file = "/etc/garm/garm-provider-lxd.toml"
```
## The credentials section
@ -154,7 +162,7 @@ docker run -d \
-p 80:80 \
-v /etc/garm:/etc/garm:rw \
-v /var/snap/lxd/common/lxd/unix.socket:/var/snap/lxd/common/lxd/unix.socket:rw \
ghcr.io/cloudbase/garm:v0.1.3
ghcr.io/cloudbase/garm:v0.1.4
```
You will notice we also mounted the LXD unix socket from the host inside the container where the config you pasted expects to find it. If you plan to use an external provider that does not need to connect to LXD over a unix socket, feel free to remove that mount.
@ -187,7 +195,7 @@ Adding the `garm` user to the LXD group will allow it to connect to the LXD unix
Next, download the latest release from the [releases page](https://github.com/cloudbase/garm/releases).
```bash
wget -q -O - https://github.com/cloudbase/garm/releases/download/v0.1.3/garm-linux-amd64.tgz | tar xzf - -C /usr/local/bin/
wget -q -O - https://github.com/cloudbase/garm/releases/download/v0.1.4/garm-linux-amd64.tgz | tar xzf - -C /usr/local/bin/
```
We'll be running under an unprivileged user. If we want to be able to listen on any port under `1024`, we'll have to set some capabilities on the binary:
@ -196,6 +204,18 @@ We'll be running under an unprivileged user. If we want to be able to listen on
setcap cap_net_bind_service=+ep /usr/local/bin/garm
```
Create a folder for the external providers:
```bash
sudo mkdir -p /opt/garm/providers.d
```
Download the LXD provider binary:
```bash
wget -q -O - https://github.com/cloudbase/garm-provider-lxd/releases/download/v0.1.0/garm-linux-amd64.tgz | sudo tar xzf - -C /opt/garm/providers.d/
```
Change the permissions on the config dir:
```bash

20
go.mod
View file

@ -18,7 +18,6 @@ require (
github.com/jedib0t/go-pretty/v6 v6.4.6
github.com/juju/clock v1.0.3
github.com/juju/retry v1.0.0
github.com/lxc/lxd v0.0.0-20230325180147-8d608287b0ce
github.com/manifoldco/promptui v0.9.0
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354
github.com/pkg/errors v0.9.1
@ -43,12 +42,8 @@ require (
github.com/chzyer/readline v1.5.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3 // indirect
github.com/frankban/quicktest v1.14.3 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-macaroon-bakery/macaroon-bakery/v3 v3.0.1 // indirect
github.com/go-macaroon-bakery/macaroonpb v1.0.0 // indirect
github.com/go-openapi/analysis v0.21.4 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
@ -63,11 +58,8 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/juju/errors v1.0.0 // indirect
github.com/juju/loggo v1.0.0 // indirect
github.com/juju/testing v1.0.2 // indirect
github.com/juju/webbrowser v1.0.0 // indirect
github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
@ -78,17 +70,11 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pborman/uuid v1.2.1 // indirect
github.com/pkg/sftp v1.13.5 // indirect
github.com/pkg/xattr v0.4.9 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/fastuuid v1.2.0 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569 // indirect
@ -97,12 +83,8 @@ require (
go.opentelemetry.io/otel/trace v1.14.0 // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/term v0.11.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/errgo.v1 v1.0.1 // indirect
gopkg.in/httprequest.v1 v1.2.1 // indirect
gopkg.in/macaroon.v2 v2.1.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

132
go.sum
View file

@ -1,4 +1,3 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
@ -9,7 +8,6 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@ -21,7 +19,6 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudbase/garm-provider-common v0.1.1-0.20231012061429-49001794e700 h1:ZCJ1zZ2WI/37ffzpRsu7t5zzShAMThhYsXw7bBNKBR0=
github.com/cloudbase/garm-provider-common v0.1.1-0.20231012061429-49001794e700/go.mod h1:igxJRT3OlykERYc6ssdRQXcb+BCaeSfnucg6I0OSoDc=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
@ -29,28 +26,14 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3 h1:fmFk0Wt3bBxxwZnu48jqMdaOR/IZ4vdtJFuaFV8MpIE=
github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3/go.mod h1:bJWSKrZyQvfTnb2OudyUjurSG4/edverV7n82+K3JiM=
github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k=
github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20=
github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-macaroon-bakery/macaroon-bakery/v3 v3.0.1 h1:uvQJoKTHrFFu8zxoaopNKedRzwdy3+8H72we4T/5cGs=
github.com/go-macaroon-bakery/macaroon-bakery/v3 v3.0.1/go.mod h1:H59IYeChwvD1po3dhGUPvq5na+4NVD7SJlbhGKvslr0=
github.com/go-macaroon-bakery/macaroonpb v1.0.0 h1:It9exBaRMZ9iix1iJ6gwzfwsDE6ExNuwtAJ9e09v6XE=
github.com/go-macaroon-bakery/macaroonpb v1.0.0/go.mod h1:UzrGOcbiwTXISFP2XDLDPjfhMINZa+fX/7A2lMd31zc=
github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY=
github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc=
github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo=
@ -119,39 +102,21 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v55 v55.0.1-0.20230921135834-aa3fcbe7aabc h1:wZybOt4gfOPJmwpe3CZFJYoREaqgngGeo1Y29zZePhg=
github.com/google/go-github/v55 v55.0.1-0.20230921135834-aa3fcbe7aabc/go.mod h1:dx9O5B1Z9+WYDRfSIkPdJ/jszShiNtl++jbgL/3OM2c=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -190,45 +155,32 @@ github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
github.com/juju/clock v1.0.3 h1:yJHIsWXeU8j3QcBdiess09SzfiXRRrsjKPn2whnMeds=
github.com/juju/clock v1.0.3/go.mod h1:HIBvJ8kiV/n7UHwKuCkdYL4l/MDECztHR2sAvWDxxf0=
github.com/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM=
github.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8=
github.com/juju/loggo v1.0.0 h1:Y6ZMQOGR9Aj3BGkiWx7HBbIx6zNwNkxhVNOHU2i1bl0=
github.com/juju/loggo v1.0.0/go.mod h1:NIXFioti1SmKAlKNuUwbMenNdef59IF52+ZzuOmHYkg=
github.com/juju/qthttptest v0.1.1/go.mod h1:aTlAv8TYaflIiTDIQYzxnl1QdPjAg8Q8qJMErpKy6A4=
github.com/juju/qthttptest v0.1.3 h1:M0HdpwsK/UTHRGRcIw5zvh5z+QOgdqyK+ecDMN+swwM=
github.com/juju/qthttptest v0.1.3/go.mod h1:2gayREyVSs/IovPmwYAtU+HZzuhDjytJQRRLzPTtDYE=
github.com/juju/retry v1.0.0 h1:Tb1hFdDSPGLH/BGdYQOF7utQ9lA0ouVJX2imqgJK6tk=
github.com/juju/retry v1.0.0/go.mod h1:SssN1eYeK3A2qjnFGTiVMbdzGJ2BfluaJblJXvuvgqA=
github.com/juju/testing v1.0.2 h1:OR90RqCd9CJONxXamZAjLknpZdtqDyxqW8IwCbgw3i4=
github.com/juju/testing v1.0.2/go.mod h1:h3Vd2rzB57KrdsBEy6R7bmSKPzP76BnNavt7i8PerwQ=
github.com/juju/utils/v3 v3.0.0 h1:Gg3n63mGPbBuoXCo+EPJuMi44hGZfloI8nlCIebHu2Q=
github.com/juju/utils/v3 v3.0.0/go.mod h1:8csUcj1VRkfjNIRzBFWzLFCMLwLqsRWvkmhfVAUwbC4=
github.com/juju/webbrowser v1.0.0 h1:JLdmbFtCGY6Qf2jmS6bVaenJFGIFkdF1/BjUm76af78=
github.com/juju/webbrowser v1.0.0/go.mod h1:RwVlbBcF91Q4vS+iwlkJ6bZTE3EwlrjbYlM3WMVD6Bc=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lxc/lxd v0.0.0-20230325180147-8d608287b0ce h1:3zb1HRvOAHOMZ8VGTDEBkKpCUVlF28zalZcb7RFjMnE=
github.com/lxc/lxd v0.0.0-20230325180147-8d608287b0ce/go.mod h1:JJ1ShHzaOzMzU0B5TNcdI9+vq8Y45ijVeNYxE1wJ8zM=
github.com/lunixbochs/vtclean v0.0.0-20160125035106-4fbf7632a2c6/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
@ -238,6 +190,8 @@ github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYt
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/mattn/go-colorable v0.0.6/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@ -264,8 +218,6 @@ github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -273,16 +225,11 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go=
github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
@ -292,22 +239,15 @@ github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.7.1-0.20230723113155-fd865a44e3c4 h1:6be13R0JVLZN659yPzYYO0O1nYeSByDy5eqi85JKG/Y=
github.com/spf13/cobra v1.7.1-0.20230723113155-fd865a44e3c4/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
@ -339,7 +279,6 @@ github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23n
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg=
go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng=
go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8=
@ -351,39 +290,22 @@ go.opentelemetry.io/otel/sdk v1.14.0 h1:PDCppFRDq8A1jL9v6KMI6dYesaq+DFcDZvjsoGvx
go.opentelemetry.io/otel/sdk v1.14.0/go.mod h1:bwIC5TjrNG6QDCHNWvW4HLHtUQ4I+VQDsnjhvyZCALM=
go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M=
go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8=
golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU=
golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -392,7 +314,6 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -401,22 +322,16 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -425,38 +340,13 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
@ -464,25 +354,15 @@ google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 h1:FVCohIoYO7IJoDDVpV2pdq7SgrMH6wHnuTyrdrxJNoY=
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v1 v1.0.0/go.mod h1:CxwszS/Xz1C49Ucd2i6Zil5UToP1EmyrFhKaMVbg1mk=
gopkg.in/errgo.v1 v1.0.1 h1:oQFRXzZ7CkBGdm1XZm/EbQYaYNNEElNBOd09M6cqNso=
gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/httprequest.v1 v1.2.1 h1:pEPLMdF/gjWHnKxLpuCYaHFjc8vAB2wrYjXrqDVC16E=
gopkg.in/httprequest.v1 v1.2.1/go.mod h1:x2Otw96yda5+8+6ZeWwHIJTFkEHWP/qP8pJOzqEtWPM=
gopkg.in/macaroon.v2 v2.1.0 h1:HZcsjBCzq9t0eBPMKqTN/uSN6JOm78ZJ2INbqcBQOUI=
gopkg.in/macaroon.v2 v2.1.0/go.mod h1:OUb+TQP/OP0WOerC2Jp/3CwhIKyIa9kQjuc7H24e6/o=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
@ -506,5 +386,3 @@ gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View file

@ -1,89 +0,0 @@
// Copyright 2022 Cloudbase Solutions SRL
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package lxd
import (
"fmt"
"strings"
runnerErrors "github.com/cloudbase/garm-provider-common/errors"
"github.com/cloudbase/garm/config"
lxd "github.com/lxc/lxd/client"
"github.com/lxc/lxd/shared/api"
"github.com/pkg/errors"
)
type image struct {
remotes map[string]config.LXDImageRemote
}
// parseImageName parses the image name that comes in from the config and returns a
// remote. If no remote is configured with the given name, an error is returned.
func (i *image) parseImageName(imageName string) (config.LXDImageRemote, string, error) {
if !strings.Contains(imageName, ":") {
return config.LXDImageRemote{}, "", fmt.Errorf("image does not include a remote")
}
details := strings.SplitN(imageName, ":", 2)
for remoteName, val := range i.remotes {
if remoteName == details[0] {
return val, details[1], nil
}
}
return config.LXDImageRemote{}, "", runnerErrors.ErrNotFound
}
func (i *image) getLocalImageByAlias(imageName string, imageType config.LXDImageType, arch string, cli lxd.InstanceServer) (*api.Image, error) {
aliases, err := cli.GetImageAliasArchitectures(imageType.String(), imageName)
if err != nil {
return nil, errors.Wrapf(err, "resolving alias: %s", imageName)
}
alias, ok := aliases[arch]
if !ok {
return nil, fmt.Errorf("no image found for arch %s and image type %s with name %s", arch, imageType, imageName)
}
image, _, err := cli.GetImage(alias.Target)
if err != nil {
return nil, errors.Wrap(err, "fetching image details")
}
return image, nil
}
func (i *image) getInstanceSource(imageName string, imageType config.LXDImageType, arch string, cli lxd.InstanceServer) (api.InstanceSource, error) {
instanceSource := api.InstanceSource{
Type: "image",
}
if !strings.Contains(imageName, ":") {
// A remote was not specified, try to find an image using the imageName as
// an alias.
imageDetails, err := i.getLocalImageByAlias(imageName, imageType, arch, cli)
if err != nil {
return api.InstanceSource{}, errors.Wrap(err, "fetching image")
}
instanceSource.Fingerprint = imageDetails.Fingerprint
} else {
remote, parsedName, err := i.parseImageName(imageName)
if err != nil {
return api.InstanceSource{}, errors.Wrap(err, "parsing image name")
}
instanceSource.Alias = parsedName
instanceSource.Server = remote.Address
instanceSource.Protocol = string(remote.Protocol)
}
return instanceSource, nil
}

View file

@ -1,530 +0,0 @@
// Copyright 2022 Cloudbase Solutions SRL
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package lxd
import (
"context"
"fmt"
"log"
"sync"
"time"
runnerErrors "github.com/cloudbase/garm-provider-common/errors"
"github.com/cloudbase/garm/config"
"github.com/cloudbase/garm/params"
"github.com/cloudbase/garm/runner/common"
lxd "github.com/lxc/lxd/client"
"github.com/lxc/lxd/shared/api"
"github.com/pkg/errors"
"github.com/cloudbase/garm-provider-common/cloudconfig"
commonParams "github.com/cloudbase/garm-provider-common/params"
)
var _ common.Provider = &LXD{}
const (
// We look for this key in the config of the instances to determine if they are
// created by us or not.
controllerIDKeyName = "user.runner-controller-id"
poolIDKey = "user.runner-pool-id"
// osTypeKeyName is the key we use in the instance config to indicate the OS
// platform a runner is supposed to have. This value is defined in the pool and
// passed into the provider as bootstrap params.
osTypeKeyName = "user.os-type"
// osArchKeyNAme is the key we use in the instance config to indicate the OS
// architecture a runner is supposed to have. This value is defined in the pool and
// passed into the provider as bootstrap params.
osArchKeyNAme = "user.os-arch"
)
var (
// lxdToGithubArchMap translates LXD architectures to Github tools architectures.
// TODO: move this in a separate package. This will most likely be used
// by any other provider.
lxdToGithubArchMap map[string]string = map[string]string{
"x86_64": "x64",
"amd64": "x64",
"armv7l": "arm",
"aarch64": "arm64",
"x64": "x64",
"arm": "arm",
"arm64": "arm64",
}
configToLXDArchMap map[commonParams.OSArch]string = map[commonParams.OSArch]string{
commonParams.Amd64: "x86_64",
commonParams.Arm64: "aarch64",
commonParams.Arm: "armv7l",
}
lxdToConfigArch map[string]commonParams.OSArch = map[string]commonParams.OSArch{
"x86_64": commonParams.Amd64,
"aarch64": commonParams.Arm64,
"armv7l": commonParams.Arm,
}
)
const (
DefaultProjectDescription = "This project was created automatically by garm to be used for github ephemeral action runners."
DefaultProjectName = "garm-project"
)
func NewProvider(ctx context.Context, cfg *config.Provider, controllerID string) (common.Provider, error) {
if err := cfg.Validate(); err != nil {
return nil, errors.Wrap(err, "validating provider config")
}
if cfg.ProviderType != params.LXDProvider {
return nil, fmt.Errorf("invalid provider type %s, expected %s", cfg.ProviderType, params.LXDProvider)
}
provider := &LXD{
ctx: ctx,
cfg: cfg,
controllerID: controllerID,
imageManager: &image{
remotes: cfg.LXD.ImageRemotes,
},
}
return provider, nil
}
type LXD struct {
// cfg is the provider config for this provider.
cfg *config.Provider
// ctx is the context.
ctx context.Context
// cli is the LXD client.
cli lxd.InstanceServer
// imageManager downloads images from remotes
imageManager *image
// controllerID is the ID of this controller
controllerID string
mux sync.Mutex
}
func (l *LXD) getCLI() (lxd.InstanceServer, error) {
l.mux.Lock()
defer l.mux.Unlock()
if l.cli != nil {
return l.cli, nil
}
cli, err := getClientFromConfig(l.ctx, &l.cfg.LXD)
if err != nil {
return nil, errors.Wrap(err, "creating LXD client")
}
_, _, err = cli.GetProject(projectName(l.cfg.LXD))
if err != nil {
return nil, errors.Wrapf(err, "fetching project name: %s", projectName(l.cfg.LXD))
}
cli = cli.UseProject(projectName(l.cfg.LXD))
l.cli = cli
return cli, nil
}
func (l *LXD) getProfiles(flavor string) ([]string, error) {
ret := []string{}
if l.cfg.LXD.IncludeDefaultProfile {
ret = append(ret, "default")
}
set := map[string]struct{}{}
cli, err := l.getCLI()
if err != nil {
return nil, errors.Wrap(err, "fetching client")
}
profiles, err := cli.GetProfileNames()
if err != nil {
return nil, errors.Wrap(err, "fetching profile names")
}
for _, profile := range profiles {
set[profile] = struct{}{}
}
if _, ok := set[flavor]; !ok {
return nil, errors.Wrapf(runnerErrors.ErrNotFound, "looking for profile %s", flavor)
}
ret = append(ret, flavor)
return ret, nil
}
func (l *LXD) getTools(tools []commonParams.RunnerApplicationDownload, osType commonParams.OSType, architecture string) (commonParams.RunnerApplicationDownload, error) {
// Validate image OS. Linux only for now.
switch osType {
case commonParams.Linux:
default:
return commonParams.RunnerApplicationDownload{}, fmt.Errorf("this provider does not support OS type: %s", osType)
}
// Find tools for OS/Arch.
for _, tool := range tools {
if tool.GetOS() == "" || tool.GetArchitecture() == "" {
continue
}
// fmt.Println(*tool.Architecture, *tool.OS)
// fmt.Printf("image arch: %s --> osType: %s\n", image.Architecture, string(osType))
if tool.GetArchitecture() == architecture && tool.GetOS() == string(osType) {
return tool, nil
}
arch, ok := lxdToGithubArchMap[architecture]
if ok && arch == tool.GetArchitecture() && tool.GetOS() == string(osType) {
return tool, nil
}
}
return commonParams.RunnerApplicationDownload{}, fmt.Errorf("failed to find tools for OS %s and arch %s", osType, architecture)
}
// sadly, the security.secureboot flag is a string encoded boolean.
func (l *LXD) secureBootEnabled() string {
if l.cfg.LXD.SecureBoot {
return "true"
}
return "false"
}
func (l *LXD) getCreateInstanceArgs(bootstrapParams commonParams.BootstrapInstance, specs extraSpecs) (api.InstancesPost, error) {
if bootstrapParams.Name == "" {
return api.InstancesPost{}, runnerErrors.NewBadRequestError("missing name")
}
profiles, err := l.getProfiles(bootstrapParams.Flavor)
if err != nil {
return api.InstancesPost{}, errors.Wrap(err, "fetching profiles")
}
arch, err := resolveArchitecture(bootstrapParams.OSArch)
if err != nil {
return api.InstancesPost{}, errors.Wrap(err, "fetching archictecture")
}
instanceType := l.cfg.LXD.GetInstanceType()
instanceSource, err := l.imageManager.getInstanceSource(bootstrapParams.Image, instanceType, arch, l.cli)
if err != nil {
return api.InstancesPost{}, errors.Wrap(err, "getting instance source")
}
tools, err := l.getTools(bootstrapParams.Tools, bootstrapParams.OSType, arch)
if err != nil {
return api.InstancesPost{}, errors.Wrap(err, "getting tools")
}
bootstrapParams.UserDataOptions.DisableUpdatesOnBoot = specs.DisableUpdates
bootstrapParams.UserDataOptions.ExtraPackages = specs.ExtraPackages
bootstrapParams.UserDataOptions.EnableBootDebug = specs.EnableBootDebug
cloudCfg, err := cloudconfig.GetCloudConfig(bootstrapParams, tools, bootstrapParams.Name)
if err != nil {
return api.InstancesPost{}, errors.Wrap(err, "generating cloud-config")
}
configMap := map[string]string{
"user.user-data": cloudCfg,
osTypeKeyName: string(bootstrapParams.OSType),
osArchKeyNAme: string(bootstrapParams.OSArch),
controllerIDKeyName: l.controllerID,
poolIDKey: bootstrapParams.PoolID,
}
if instanceType == config.LXDImageVirtualMachine {
configMap["security.secureboot"] = l.secureBootEnabled()
}
args := api.InstancesPost{
InstancePut: api.InstancePut{
Architecture: arch,
Profiles: profiles,
Description: "Github runner provisioned by garm",
Config: configMap,
},
Source: instanceSource,
Name: bootstrapParams.Name,
Type: api.InstanceType(instanceType),
}
return args, nil
}
func (l *LXD) AsParams() params.Provider {
return params.Provider{
Name: l.cfg.Name,
ProviderType: l.cfg.ProviderType,
Description: l.cfg.Description,
}
}
func (l *LXD) launchInstance(createArgs api.InstancesPost) error {
cli, err := l.getCLI()
if err != nil {
return errors.Wrap(err, "fetching client")
}
// Get LXD to create the instance (background operation)
op, err := cli.CreateInstance(createArgs)
if err != nil {
return errors.Wrap(err, "creating instance")
}
// Wait for the operation to complete
err = op.Wait()
if err != nil {
return errors.Wrap(err, "waiting for instance creation")
}
// Get LXD to start the instance (background operation)
reqState := api.InstanceStatePut{
Action: "start",
Timeout: -1,
}
op, err = cli.UpdateInstanceState(createArgs.Name, reqState, "")
if err != nil {
return errors.Wrap(err, "starting instance")
}
// Wait for the operation to complete
err = op.Wait()
if err != nil {
return errors.Wrap(err, "waiting for instance to start")
}
return nil
}
// CreateInstance creates a new compute instance in the provider.
func (l *LXD) CreateInstance(ctx context.Context, bootstrapParams commonParams.BootstrapInstance) (commonParams.ProviderInstance, error) {
extraSpecs, err := parseExtraSpecsFromBootstrapParams(bootstrapParams)
if err != nil {
return commonParams.ProviderInstance{}, errors.Wrap(err, "parsing extra specs")
}
args, err := l.getCreateInstanceArgs(bootstrapParams, extraSpecs)
if err != nil {
return commonParams.ProviderInstance{}, errors.Wrap(err, "fetching create args")
}
if err := l.launchInstance(args); err != nil {
return commonParams.ProviderInstance{}, errors.Wrap(err, "creating instance")
}
ret, err := l.waitInstanceHasIP(ctx, args.Name)
if err != nil {
return commonParams.ProviderInstance{}, errors.Wrap(err, "fetching instance")
}
return ret, nil
}
// GetInstance will return details about one instance.
func (l *LXD) GetInstance(ctx context.Context, instanceName string) (commonParams.ProviderInstance, error) {
cli, err := l.getCLI()
if err != nil {
return commonParams.ProviderInstance{}, errors.Wrap(err, "fetching client")
}
instance, _, err := cli.GetInstanceFull(instanceName)
if err != nil {
if isNotFoundError(err) {
return commonParams.ProviderInstance{}, errors.Wrapf(runnerErrors.ErrNotFound, "fetching instance: %q", err)
}
return commonParams.ProviderInstance{}, errors.Wrap(err, "fetching instance")
}
return lxdInstanceToAPIInstance(instance), nil
}
// Delete instance will delete the instance in a provider.
func (l *LXD) DeleteInstance(ctx context.Context, instance string) error {
cli, err := l.getCLI()
if err != nil {
return errors.Wrap(err, "fetching client")
}
if err := l.setState(instance, "stop", true); err != nil {
if isNotFoundError(err) {
log.Printf("received not found error when stopping instance %s", instance)
return nil
}
// I am not proud of this, but the drivers.ErrInstanceIsStopped from LXD pulls in
// a ton of CGO, linux specific dependencies, that don't make sense having
// in garm.
if !(errors.Cause(err).Error() == errInstanceIsStopped.Error()) {
return errors.Wrap(err, "stopping instance")
}
}
opResponse := make(chan struct {
op lxd.Operation
err error
})
var op lxd.Operation
go func() {
op, err := cli.DeleteInstance(instance)
opResponse <- struct {
op lxd.Operation
err error
}{op: op, err: err}
}()
select {
case resp := <-opResponse:
if resp.err != nil {
if isNotFoundError(resp.err) {
log.Printf("received not found error when deleting instance %s", instance)
return nil
}
return errors.Wrap(resp.err, "removing instance")
}
op = resp.op
case <-time.After(time.Second * 60):
return errors.Wrapf(runnerErrors.ErrTimeout, "removing instance %s", instance)
}
opTimeout, cancel := context.WithTimeout(context.Background(), time.Second*60)
defer cancel()
err = op.WaitContext(opTimeout)
if err != nil {
if isNotFoundError(err) {
log.Printf("received not found error when waiting for instance deletion %s", instance)
return nil
}
return errors.Wrap(err, "waiting for instance deletion")
}
return nil
}
type listResponse struct {
instances []api.InstanceFull
err error
}
// ListInstances will list all instances for a provider.
func (l *LXD) ListInstances(ctx context.Context, poolID string) ([]commonParams.ProviderInstance, error) {
cli, err := l.getCLI()
if err != nil {
return []commonParams.ProviderInstance{}, errors.Wrap(err, "fetching client")
}
result := make(chan listResponse, 1)
go func() {
// TODO(gabriel-samfira): if this blocks indefinitely, we will leak a goroutine.
// Convert the internal provider to an external one. Running the provider as an
// external process will allow us to not care if a goroutine leaks. Once a timeout
// is reached, the provider can just exit with an error. Something we can't do with
// internal providers.
instances, err := cli.GetInstancesFull(api.InstanceTypeAny)
result <- listResponse{
instances: instances,
err: err,
}
}()
var instances []api.InstanceFull
select {
case res := <-result:
if res.err != nil {
return []commonParams.ProviderInstance{}, errors.Wrap(res.err, "fetching instances")
}
instances = res.instances
case <-time.After(time.Second * 60):
return []commonParams.ProviderInstance{}, errors.Wrap(runnerErrors.ErrTimeout, "fetching instances from provider")
}
ret := []commonParams.ProviderInstance{}
for _, instance := range instances {
if id, ok := instance.ExpandedConfig[controllerIDKeyName]; ok && id == l.controllerID {
if poolID != "" {
id := instance.ExpandedConfig[poolIDKey]
if id != poolID {
// Pool ID was specified. Filter out instances belonging to other pools.
continue
}
}
ret = append(ret, lxdInstanceToAPIInstance(&instance))
}
}
return ret, nil
}
// RemoveAllInstances will remove all instances created by this provider.
func (l *LXD) RemoveAllInstances(ctx context.Context) error {
instances, err := l.ListInstances(ctx, "")
if err != nil {
return errors.Wrap(err, "fetching instance list")
}
for _, instance := range instances {
// TODO: remove in parallel
if err := l.DeleteInstance(ctx, instance.Name); err != nil {
return errors.Wrapf(err, "removing instance %s", instance.Name)
}
}
return nil
}
func (l *LXD) setState(instance, state string, force bool) error {
reqState := api.InstanceStatePut{
Action: state,
Timeout: -1,
Force: force,
}
cli, err := l.getCLI()
if err != nil {
return errors.Wrap(err, "fetching client")
}
op, err := cli.UpdateInstanceState(instance, reqState, "")
if err != nil {
return errors.Wrapf(err, "setting state to %s", state)
}
ctxTimeout, cancel := context.WithTimeout(context.Background(), time.Second*60)
defer cancel()
err = op.WaitContext(ctxTimeout)
if err != nil {
return errors.Wrapf(err, "waiting for instance to transition to state %s", state)
}
return nil
}
// Stop shuts down the instance.
func (l *LXD) Stop(ctx context.Context, instance string, force bool) error {
return l.setState(instance, "stop", force)
}
// Start boots up an instance.
func (l *LXD) Start(ctx context.Context, instance string) error {
return l.setState(instance, "start", false)
}
// DisableJITConfig tells us if the provider explicitly disables JIT configuration and
// forces runner registration tokens to be used. This may happen if a provider has not yet
// been updated to support JIT configuration.
func (l *LXD) DisableJITConfig() bool {
if l.cfg == nil {
return false
}
return l.cfg.DisableJITConfig
}

View file

@ -1,40 +0,0 @@
// Copyright 2023 Cloudbase Solutions SRL
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package lxd
import (
"encoding/json"
commonParams "github.com/cloudbase/garm-provider-common/params"
"github.com/pkg/errors"
)
type extraSpecs struct {
DisableUpdates bool `json:"disable_updates"`
ExtraPackages []string `json:"extra_packages"`
EnableBootDebug bool `json:"enable_boot_debug"`
}
func parseExtraSpecsFromBootstrapParams(bootstrapParams commonParams.BootstrapInstance) (extraSpecs, error) {
specs := extraSpecs{}
if bootstrapParams.ExtraSpecs == nil {
return specs, nil
}
if err := json.Unmarshal(bootstrapParams.ExtraSpecs, &specs); err != nil {
return specs, errors.Wrap(err, "unmarshaling extra specs")
}
return specs, nil
}

View file

@ -1,234 +0,0 @@
// Copyright 2022 Cloudbase Solutions SRL
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package lxd
import (
"context"
"database/sql"
"fmt"
"log"
"net"
"net/http"
"os"
"strings"
"time"
commonParams "github.com/cloudbase/garm-provider-common/params"
"github.com/cloudbase/garm-provider-common/util"
"github.com/cloudbase/garm/config"
"github.com/juju/clock"
"github.com/juju/retry"
lxd "github.com/lxc/lxd/client"
"github.com/lxc/lxd/shared/api"
"github.com/pkg/errors"
)
var (
//lint:ignore ST1005 imported error from lxd
errInstanceIsStopped error = fmt.Errorf("The instance is already stopped")
)
var httpResponseErrors = map[int][]error{
http.StatusNotFound: {os.ErrNotExist, sql.ErrNoRows},
}
// isNotFoundError returns true if the error is considered a Not Found error.
func isNotFoundError(err error) bool {
if api.StatusErrorCheck(err, http.StatusNotFound) {
return true
}
for _, checkErr := range httpResponseErrors[http.StatusNotFound] {
if errors.Is(err, checkErr) {
return true
}
}
return false
}
func lxdInstanceToAPIInstance(instance *api.InstanceFull) commonParams.ProviderInstance {
lxdOS, ok := instance.ExpandedConfig["image.os"]
if !ok {
log.Printf("failed to find OS in instance config")
}
osType, err := util.OSToOSType(lxdOS)
if err != nil {
log.Printf("failed to find OS type for OS %s", lxdOS)
}
if osType == "" {
osTypeFromTag, ok := instance.ExpandedConfig[osTypeKeyName]
if !ok {
log.Printf("failed to find OS type in fallback location")
}
osType = commonParams.OSType(osTypeFromTag)
}
osRelease, ok := instance.ExpandedConfig["image.release"]
if !ok {
log.Printf("failed to find OS release instance config")
}
state := instance.State
addresses := []commonParams.Address{}
if state.Network != nil {
for _, details := range state.Network {
for _, addr := range details.Addresses {
if addr.Scope != "global" {
continue
}
addresses = append(addresses, commonParams.Address{
Address: addr.Address,
Type: commonParams.PublicAddress,
})
}
}
}
instanceArch, ok := lxdToConfigArch[instance.Architecture]
if !ok {
log.Printf("failed to find OS architecture")
}
return commonParams.ProviderInstance{
OSArch: instanceArch,
ProviderID: instance.Name,
Name: instance.Name,
OSType: osType,
OSName: strings.ToLower(lxdOS),
OSVersion: osRelease,
Addresses: addresses,
Status: lxdStatusToProviderStatus(state.Status),
}
}
func lxdStatusToProviderStatus(status string) commonParams.InstanceStatus {
switch status {
case "Running":
return commonParams.InstanceRunning
case "Stopped":
return commonParams.InstanceStopped
default:
return commonParams.InstanceStatusUnknown
}
}
func getClientFromConfig(ctx context.Context, cfg *config.LXD) (cli lxd.InstanceServer, err error) {
if cfg.UnixSocket != "" {
return lxd.ConnectLXDUnixWithContext(ctx, cfg.UnixSocket, nil)
}
var srvCrtContents, tlsCAContents, clientCertContents, clientKeyContents []byte
if cfg.TLSServerCert != "" {
srvCrtContents, err = os.ReadFile(cfg.TLSServerCert)
if err != nil {
return nil, errors.Wrap(err, "reading TLSServerCert")
}
}
if cfg.TLSCA != "" {
tlsCAContents, err = os.ReadFile(cfg.TLSCA)
if err != nil {
return nil, errors.Wrap(err, "reading TLSCA")
}
}
if cfg.ClientCertificate != "" {
clientCertContents, err = os.ReadFile(cfg.ClientCertificate)
if err != nil {
return nil, errors.Wrap(err, "reading ClientCertificate")
}
}
if cfg.ClientKey != "" {
clientKeyContents, err = os.ReadFile(cfg.ClientKey)
if err != nil {
return nil, errors.Wrap(err, "reading ClientKey")
}
}
connectArgs := lxd.ConnectionArgs{
TLSServerCert: string(srvCrtContents),
TLSCA: string(tlsCAContents),
TLSClientCert: string(clientCertContents),
TLSClientKey: string(clientKeyContents),
}
lxdCLI, err := lxd.ConnectLXD(cfg.URL, &connectArgs)
if err != nil {
return nil, errors.Wrap(err, "connecting to LXD")
}
return lxdCLI, nil
}
func projectName(cfg config.LXD) string {
if cfg.ProjectName != "" {
return cfg.ProjectName
}
return DefaultProjectName
}
func resolveArchitecture(osArch commonParams.OSArch) (string, error) {
if string(osArch) == "" {
return configToLXDArchMap[commonParams.Amd64], nil
}
arch, ok := configToLXDArchMap[osArch]
if !ok {
return "", fmt.Errorf("architecture %s is not supported", osArch)
}
return arch, nil
}
// waitDeviceActive is a function capable of figuring out when a Equinix Metal
// device is active
func (l *LXD) waitInstanceHasIP(ctx context.Context, instanceName string) (commonParams.ProviderInstance, error) {
var p commonParams.ProviderInstance
var errIPNotFound error = fmt.Errorf("ip not found")
err := retry.Call(retry.CallArgs{
Func: func() error {
var err error
p, err = l.GetInstance(ctx, instanceName)
if err != nil {
return errors.Wrap(err, "fetching instance")
}
for _, addr := range p.Addresses {
ip := net.ParseIP(addr.Address)
if ip == nil {
continue
}
if ip.To4() == nil {
continue
}
return nil
}
return errIPNotFound
},
Attempts: 20,
Delay: 5 * time.Second,
Clock: clock.WallClock,
})
if err != nil && err != errIPNotFound {
return commonParams.ProviderInstance{}, err
}
return p, nil
}

View file

@ -22,7 +22,6 @@ import (
"github.com/cloudbase/garm/params"
"github.com/cloudbase/garm/runner/common"
"github.com/cloudbase/garm/runner/providers/external"
"github.com/cloudbase/garm/runner/providers/lxd"
"github.com/pkg/errors"
)
@ -34,13 +33,6 @@ func LoadProvidersFromConfig(ctx context.Context, cfg config.Config, controllerI
for _, providerCfg := range cfg.Providers {
log.Printf("Loading provider %s", providerCfg.Name)
switch providerCfg.ProviderType {
case params.LXDProvider:
conf := providerCfg
provider, err := lxd.NewProvider(ctx, &conf, controllerID)
if err != nil {
return nil, errors.Wrap(err, "creating provider")
}
providers[providerCfg.Name] = provider
case params.ExternalProvider:
conf := providerCfg
provider, err := external.NewProvider(ctx, &conf, controllerID)
@ -48,6 +40,8 @@ func LoadProvidersFromConfig(ctx context.Context, cfg config.Config, controllerI
return nil, errors.Wrap(err, "creating provider")
}
providers[providerCfg.Name] = provider
default:
return nil, errors.Errorf("unknown provider type %s", providerCfg.ProviderType)
}
}
return providers, nil

View file

@ -25,30 +25,11 @@ passphrase = "${DB_PASSPHRASE}"
[[provider]]
name = "lxd_local"
provider_type = "lxd"
provider_type = "external"
description = "Local LXD installation"
[provider.lxd]
unix_socket_path = "/var/snap/lxd/common/lxd/unix.socket"
include_default_profile = false
instance_type = "container"
secure_boot = false
project_name = "default"
[provider.lxd.image_remotes]
[provider.lxd.image_remotes.ubuntu]
addr = "https://cloud-images.ubuntu.com/releases"
public = true
protocol = "simplestreams"
skip_verify = false
[provider.lxd.image_remotes.ubuntu_daily]
addr = "https://cloud-images.ubuntu.com/daily"
public = true
protocol = "simplestreams"
skip_verify = false
[provider.lxd.image_remotes.images]
addr = "https://images.linuxcontainers.org"
public = true
protocol = "simplestreams"
skip_verify = false
[provider.external]
provider_executable = "${LXD_PROVIDER_EXECUTABLE}"
config_file = "${LXD_PROVIDER_CONFIG}"
[[provider]]
name = "test_external"

View file

@ -0,0 +1,21 @@
unix_socket_path = "/var/snap/lxd/common/lxd/unix.socket"
include_default_profile = false
instance_type = "container"
secure_boot = false
project_name = "default"
[image_remotes]
[image_remotes.ubuntu]
addr = "https://cloud-images.ubuntu.com/releases"
public = true
protocol = "simplestreams"
skip_verify = false
[image_remotes.ubuntu_daily]
addr = "https://cloud-images.ubuntu.com/daily"
public = true
protocol = "simplestreams"
skip_verify = false
[image_remotes.images]
addr = "https://images.linuxcontainers.org"
public = true
protocol = "simplestreams"
skip_verify = false

View file

@ -6,6 +6,9 @@ BINARIES_DIR="$PWD/bin"
CONTRIB_DIR="$PWD/contrib"
CONFIG_DIR="$PWD/test/integration/config"
CONFIG_DIR_PROV="$PWD/test/integration/provider"
PROVIDER_BIN_DIR="/opt/garm/providers.d/lxd"
LXD_PROVIDER_EXECUTABLE="$PROVIDER_BIN_DIR/garm-provider-lxd"
LXD_PROVIDER_CONFIG="$CONFIG_DIR/garm-provider-lxd.toml"
if [[ ! -f $BINARIES_DIR/garm ]] || [[ ! -f $BINARIES_DIR/garm-cli ]]; then
echo "ERROR: Please build GARM binaries first"
@ -43,6 +46,12 @@ export DB_PASSPHRASE="$(generate_secret)"
# Group "adm" is the LXD daemon group as set by the "canonical/setup-lxd" GitHub action.
sudo useradd --shell /usr/bin/false --system --groups adm --no-create-home garm
sudo mkdir -p $PROVIDER_BIN_DIR
git clone https://github.com/cloudbase/garm-provider-lxd ~/garm-provider-lxd
pushd ~/garm-provider-lxd
go build -o $PROVIDER_BIN_DIR/garm-provider-lxd
popd
sudo mkdir -p /etc/garm
cat $CONFIG_DIR/config.toml | envsubst | sudo tee /etc/garm/config.toml
sudo chown -R garm:garm /etc/garm

View file

@ -1,182 +0,0 @@
// Copyright 2022 Cloudbase Solutions SRL
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package cloudconfig
import (
"crypto/x509"
"encoding/base64"
"fmt"
"strings"
"sync"
"github.com/cloudbase/garm-provider-common/defaults"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)
func NewDefaultCloudInitConfig() *CloudInit {
return &CloudInit{
PackageUpgrade: true,
Packages: []string{
"curl",
"tar",
},
SystemInfo: &SystemInfo{
DefaultUser: DefaultUser{
Name: defaults.DefaultUser,
Home: fmt.Sprintf("/home/%s", defaults.DefaultUser),
Shell: defaults.DefaultUserShell,
Groups: defaults.DefaultUserGroups,
Sudo: "ALL=(ALL) NOPASSWD:ALL",
},
},
}
}
type DefaultUser struct {
Name string `yaml:"name"`
Home string `yaml:"home"`
Shell string `yaml:"shell"`
Groups []string `yaml:"groups,omitempty"`
Sudo string `yaml:"sudo"`
}
type SystemInfo struct {
DefaultUser DefaultUser `yaml:"default_user"`
}
type File struct {
Encoding string `yaml:"encoding"`
Content string `yaml:"content"`
Owner string `yaml:"owner"`
Path string `yaml:"path"`
Permissions string `yaml:"permissions"`
}
type CloudInit struct {
mux sync.Mutex
PackageUpgrade bool `yaml:"package_upgrade"`
Packages []string `yaml:"packages,omitempty"`
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys,omitempty"`
SystemInfo *SystemInfo `yaml:"system_info,omitempty"`
RunCmd []string `yaml:"runcmd,omitempty"`
WriteFiles []File `yaml:"write_files,omitempty"`
CACerts CACerts `yaml:"ca-certs,omitempty"`
}
type CACerts struct {
RemoveDefaults bool `yaml:"remove-defaults"`
Trusted []string `yaml:"trusted"`
}
func (c *CloudInit) AddCACert(cert []byte) error {
c.mux.Lock()
defer c.mux.Unlock()
if cert == nil {
return nil
}
roots := x509.NewCertPool()
if ok := roots.AppendCertsFromPEM(cert); !ok {
return fmt.Errorf("failed to parse CA cert bundle")
}
c.CACerts.Trusted = append(c.CACerts.Trusted, string(cert))
return nil
}
func (c *CloudInit) AddSSHKey(keys ...string) {
c.mux.Lock()
defer c.mux.Unlock()
// TODO(gabriel-samfira): Validate the SSH public key.
for _, key := range keys {
found := false
for _, val := range c.SSHAuthorizedKeys {
if val == key {
found = true
break
}
}
if !found {
c.SSHAuthorizedKeys = append(c.SSHAuthorizedKeys, key)
}
}
}
func (c *CloudInit) AddPackage(pkgs ...string) {
c.mux.Lock()
defer c.mux.Unlock()
for _, pkg := range pkgs {
found := false
for _, val := range c.Packages {
if val == pkg {
found = true
break
}
}
if !found {
c.Packages = append(c.Packages, pkg)
}
}
}
func (c *CloudInit) AddRunCmd(cmd string) {
c.mux.Lock()
defer c.mux.Unlock()
c.RunCmd = append(c.RunCmd, cmd)
}
func (c *CloudInit) AddFile(contents []byte, path, owner, permissions string) {
c.mux.Lock()
defer c.mux.Unlock()
for _, val := range c.WriteFiles {
if val.Path == path {
return
}
}
file := File{
Encoding: "b64",
Content: base64.StdEncoding.EncodeToString(contents),
Owner: owner,
Permissions: permissions,
Path: path,
}
c.WriteFiles = append(c.WriteFiles, file)
}
func (c *CloudInit) Serialize() (string, error) {
c.mux.Lock()
defer c.mux.Unlock()
ret := []string{
"#cloud-config",
}
asYaml, err := yaml.Marshal(c)
if err != nil {
return "", errors.Wrap(err, "marshaling to yaml")
}
ret = append(ret, string(asYaml))
return strings.Join(ret, "\n"), nil
}

View file

@ -1,539 +0,0 @@
// Copyright 2022 Cloudbase Solutions SRL
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package cloudconfig
import (
"bytes"
"fmt"
"text/template"
"github.com/cloudbase/garm-provider-common/params"
"github.com/pkg/errors"
)
var CloudConfigTemplate = `#!/bin/bash
set -e
set -o pipefail
{{- if .EnableBootDebug }}
set -x
{{- end }}
CALLBACK_URL="{{ .CallbackURL }}"
METADATA_URL="{{ .MetadataURL }}"
BEARER_TOKEN="{{ .CallbackToken }}"
if [ -z "$METADATA_URL" ];then
echo "no token is available and METADATA_URL is not set"
exit 1
fi
function call() {
PAYLOAD="$1"
[[ $CALLBACK_URL =~ ^(.*)/status$ ]] || CALLBACK_URL="${CALLBACK_URL}/status"
curl --retry 5 --retry-delay 5 --retry-connrefused --fail -s -X POST -d "${PAYLOAD}" -H 'Accept: application/json' -H "Authorization: Bearer ${BEARER_TOKEN}" "${CALLBACK_URL}" || echo "failed to call home: exit code ($?)"
}
function sendStatus() {
MSG="$1"
call "{\"status\": \"installing\", \"message\": \"$MSG\"}"
}
{{- if .UseJITConfig }}
function success() {
MSG="$1"
call "{\"status\": \"idle\", \"message\": \"$MSG\"}"
}
{{- else}}
function success() {
MSG="$1"
ID=$2
call "{\"status\": \"idle\", \"message\": \"$MSG\", \"agent_id\": $ID}"
}
{{- end}}
function fail() {
MSG="$1"
call "{\"status\": \"failed\", \"message\": \"$MSG\"}"
exit 1
}
# This will echo the version number in the filename. Given a file name like: actions-runner-osx-x64-2.299.1.tar.gz
# this will output: 2.299.1
function getRunnerVersion() {
FILENAME="{{ .FileName }}"
[[ $FILENAME =~ ([0-9]+\.[0-9]+\.[0-9+]) ]]
echo $BASH_REMATCH
}
function getCachedToolsPath() {
CACHED_RUNNER="/opt/cache/actions-runner/latest"
if [ -d "$CACHED_RUNNER" ];then
echo "$CACHED_RUNNER"
return 0
fi
VERSION=$(getRunnerVersion)
if [ -z "$VERSION" ]; then
return 0
fi
CACHED_RUNNER="/opt/cache/actions-runner/$VERSION"
if [ -d "$CACHED_RUNNER" ];then
echo "$CACHED_RUNNER"
return 0
fi
return 0
}
function downloadAndExtractRunner() {
sendStatus "downloading tools from {{ .DownloadURL }}"
if [ ! -z "{{ .TempDownloadToken }}" ]; then
TEMP_TOKEN="Authorization: Bearer {{ .TempDownloadToken }}"
fi
curl --retry 5 --retry-delay 5 --retry-connrefused --fail -L -H "${TEMP_TOKEN}" -o "/home/{{ .RunnerUsername }}/{{ .FileName }}" "{{ .DownloadURL }}" || fail "failed to download tools"
mkdir -p /home/{{ .RunnerUsername }}/actions-runner || fail "failed to create actions-runner folder"
sendStatus "extracting runner"
tar xf "/home/{{ .RunnerUsername }}/{{ .FileName }}" -C /home/{{ .RunnerUsername }}/actions-runner/ || fail "failed to extract runner"
# chown {{ .RunnerUsername }}:{{ .RunnerGroup }} -R /home/{{ .RunnerUsername }}/actions-runner/ || fail "failed to change owner"
}
CACHED_RUNNER=$(getCachedToolsPath)
if [ -z "$CACHED_RUNNER" ];then
downloadAndExtractRunner
sendStatus "installing dependencies"
cd /home/{{ .RunnerUsername }}/actions-runner
sudo ./bin/installdependencies.sh || fail "failed to install dependencies"
else
sendStatus "using cached runner found in $CACHED_RUNNER"
sudo cp -a "$CACHED_RUNNER" "/home/{{ .RunnerUsername }}/actions-runner"
sudo chown {{ .RunnerUsername }}:{{ .RunnerGroup }} -R "/home/{{ .RunnerUsername }}/actions-runner" || fail "failed to change owner"
cd /home/{{ .RunnerUsername }}/actions-runner
fi
sendStatus "configuring runner"
{{- if .UseJITConfig }}
function getRunnerFile() {
curl --retry 5 --retry-delay 5 \
--retry-connrefused --fail -s \
-X GET -H 'Accept: application/json' \
-H "Authorization: Bearer ${BEARER_TOKEN}" \
"${METADATA_URL}/$1" -o "$2"
}
sendStatus "downloading JIT credentials"
getRunnerFile "credentials/runner" "/home/{{ .RunnerUsername }}/actions-runner/.runner" || fail "failed to get runner file"
getRunnerFile "credentials/credentials" "/home/{{ .RunnerUsername }}/actions-runner/.credentials" || fail "failed to get credentials file"
getRunnerFile "credentials/credentials_rsaparams" "/home/{{ .RunnerUsername }}/actions-runner/.credentials_rsaparams" || fail "failed to get credentials_rsaparams file"
getRunnerFile "system/service-name" "/home/{{ .RunnerUsername }}/actions-runner/.service" || fail "failed to get service name file"
sed -i 's/$/\.service/' /home/{{ .RunnerUsername }}/actions-runner/.service
SVC_NAME=$(cat /home/{{ .RunnerUsername }}/actions-runner/.service)
sendStatus "generating systemd unit file"
getRunnerFile "systemd/unit-file?runAsUser={{ .RunnerUsername }}" "$SVC_NAME" || fail "failed to get service file"
sudo mv $SVC_NAME /etc/systemd/system/ || fail "failed to move service file"
sendStatus "enabling runner service"
cp /home/{{ .RunnerUsername }}/actions-runner/bin/runsvc.sh /home/{{ .RunnerUsername }}/actions-runner/ || fail "failed to copy runsvc.sh"
sudo chown {{ .RunnerUsername }}:{{ .RunnerGroup }} -R /home/{{ .RunnerUsername }} || fail "failed to change owner"
sudo systemctl daemon-reload || fail "failed to reload systemd"
sudo systemctl enable $SVC_NAME
{{- else}}
GITHUB_TOKEN=$(curl --retry 5 --retry-delay 5 --retry-connrefused --fail -s -X GET -H 'Accept: application/json' -H "Authorization: Bearer ${BEARER_TOKEN}" "${METADATA_URL}/runner-registration-token/")
set +e
attempt=1
while true; do
ERROUT=$(mktemp)
{{- if .GitHubRunnerGroup }}
./config.sh --unattended --url "{{ .RepoURL }}" --token "$GITHUB_TOKEN" --runnergroup {{.GitHubRunnerGroup}} --name "{{ .RunnerName }}" --labels "{{ .RunnerLabels }}" --ephemeral 2>$ERROUT
{{- else}}
./config.sh --unattended --url "{{ .RepoURL }}" --token "$GITHUB_TOKEN" --name "{{ .RunnerName }}" --labels "{{ .RunnerLabels }}" --ephemeral 2>$ERROUT
{{- end}}
if [ $? -eq 0 ]; then
rm $ERROUT || true
sendStatus "runner successfully configured after $attempt attempt(s)"
break
fi
LAST_ERR=$(cat $ERROUT)
echo "$LAST_ERR"
# if the runner is already configured, remove it and try again. In the past configuring a runner
# managed to register it but timed out later, resulting in an error.
./config.sh remove --token "$GITHUB_TOKEN" || true
if [ $attempt -gt 5 ];then
rm $ERROUT || true
fail "failed to configure runner: $LAST_ERR"
fi
sendStatus "failed to configure runner (attempt $attempt): $LAST_ERR (retrying in 5 seconds)"
attempt=$((attempt+1))
rm $ERROUT || true
sleep 5
done
set -e
sendStatus "installing runner service"
sudo ./svc.sh install {{ .RunnerUsername }} || fail "failed to install service"
{{- end}}
if [ -e "/sys/fs/selinux" ];then
sudo chcon -h user_u:object_r:bin_t /home/runner/ || fail "failed to change selinux context"
sudo chcon -R -h {{ .RunnerUsername }}:object_r:bin_t /home/runner/* || fail "failed to change selinux context"
fi
{{- if .UseJITConfig }}
sudo systemctl start $SVC_NAME || fail "failed to start service"
success "runner successfully installed"
{{- else}}
sendStatus "starting service"
sudo ./svc.sh start || fail "failed to start service"
set +e
AGENT_ID=$(grep "agentId" /home/{{ .RunnerUsername }}/actions-runner/.runner | tr -d -c 0-9)
if [ $? -ne 0 ];then
fail "failed to get agent ID"
fi
set -e
success "runner successfully installed" $AGENT_ID
{{- end}}
`
var WindowsSetupScriptTemplate = `#ps1_sysnative
Param(
[Parameter(Mandatory=$false)]
[string]$Token="{{.CallbackToken}}"
)
$ErrorActionPreference="Stop"
function Invoke-FastWebRequest {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True,ValueFromPipeline=$true,Position=0)]
[System.Uri]$Uri,
[Parameter(Position=1)]
[string]$OutFile,
[Hashtable]$Headers=@{},
[switch]$SkipIntegrityCheck=$false
)
PROCESS
{
if(!([System.Management.Automation.PSTypeName]'System.Net.Http.HttpClient').Type)
{
$assembly = [System.Reflection.Assembly]::LoadWithPartialName("System.Net.Http")
}
if(!$OutFile) {
$OutFile = $Uri.PathAndQuery.Substring($Uri.PathAndQuery.LastIndexOf("/") + 1)
if(!$OutFile) {
throw "The ""OutFile"" parameter needs to be specified"
}
}
$fragment = $Uri.Fragment.Trim('#')
if ($fragment) {
$details = $fragment.Split("=")
$algorithm = $details[0]
$hash = $details[1]
}
if (!$SkipIntegrityCheck -and $fragment -and (Test-Path $OutFile)) {
try {
return (Test-FileIntegrity -File $OutFile -Algorithm $algorithm -ExpectedHash $hash)
} catch {
Remove-Item $OutFile
}
}
$client = new-object System.Net.Http.HttpClient
foreach ($k in $Headers.Keys){
$client.DefaultRequestHeaders.Add($k, $Headers[$k])
}
$task = $client.GetStreamAsync($Uri)
$response = $task.Result
if($task.IsFaulted) {
$msg = "Request for URL '{0}' is faulted. Task status: {1}." -f @($Uri, $task.Status)
if($task.Exception) {
$msg += "Exception details: {0}" -f @($task.Exception)
}
Throw $msg
}
$outStream = New-Object IO.FileStream $OutFile, Create, Write, None
try {
$totRead = 0
$buffer = New-Object Byte[] 1MB
while (($read = $response.Read($buffer, 0, $buffer.Length)) -gt 0) {
$totRead += $read
$outStream.Write($buffer, 0, $read);
}
}
finally {
$outStream.Close()
}
if(!$SkipIntegrityCheck -and $fragment) {
Test-FileIntegrity -File $OutFile -Algorithm $algorithm -ExpectedHash $hash
}
}
}
function Import-Certificate() {
[CmdletBinding()]
param (
[parameter(Mandatory=$true)]
$CertificateData,
[parameter(Mandatory=$false)]
[System.Security.Cryptography.X509Certificates.StoreLocation]$StoreLocation="LocalMachine",
[parameter(Mandatory=$false)]
[System.Security.Cryptography.X509Certificates.StoreName]$StoreName="TrustedPublisher"
)
PROCESS
{
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store(
$StoreName, $StoreLocation)
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($CertificateData)
$store.Add($cert)
}
}
function Invoke-APICall() {
[CmdletBinding()]
param (
[parameter(Mandatory=$true)]
[object]$Payload,
[parameter(Mandatory=$true)]
[string]$CallbackURL
)
PROCESS{
Invoke-WebRequest -UseBasicParsing -Method Post -Headers @{"Accept"="application/json"; "Authorization"="Bearer $Token"} -Uri $CallbackURL -Body (ConvertTo-Json $Payload) | Out-Null
}
}
function Update-GarmStatus() {
[CmdletBinding()]
param (
[parameter(Mandatory=$true)]
[string]$Message,
[parameter(Mandatory=$false)]
[int64]$AgentID=0,
[parameter(Mandatory=$false)]
[string]$Status="installing",
[parameter(Mandatory=$true)]
[string]$CallbackURL
)
PROCESS{
$body = @{
"status"=$Status
"message"=$Message
}
if ($AgentID -ne 0) {
$body["AgentID"] = $AgentID
}
Invoke-APICall -Payload $body -CallbackURL $CallbackURL | Out-Null
}
}
function Invoke-GarmSuccess() {
[CmdletBinding()]
param (
[parameter(Mandatory=$true)]
[string]$Message,
[parameter(Mandatory=$true)]
[int64]$AgentID,
[parameter(Mandatory=$true)]
[string]$CallbackURL
)
PROCESS{
Update-GarmStatus -Message $Message -AgentID $AgentID -CallbackURL $CallbackURL -Status "idle" | Out-Null
}
}
function Invoke-GarmFailure() {
[CmdletBinding()]
param (
[parameter(Mandatory=$true)]
[string]$Message,
[parameter(Mandatory=$true)]
[string]$CallbackURL
)
PROCESS{
Update-GarmStatus -Message $Message -CallbackURL $CallbackURL -Status "failed" | Out-Null
Throw $Message
}
}
$GHRunnerGroup = "{{.GitHubRunnerGroup}}"
function Install-Runner() {
$CallbackURL="{{.CallbackURL}}"
if (!$CallbackURL.EndsWith("/status")) {
$CallbackURL = "$CallbackURL/status"
}
if ($Token.Length -eq 0) {
Throw "missing callback authentication token"
}
try {
$MetadataURL="{{.MetadataURL}}"
$DownloadURL="{{.DownloadURL}}"
if($MetadataURL -eq ""){
Throw "missing metadata URL"
}
$bundle = wget -UseBasicParsing -Headers @{"Accept"="application/json"; "Authorization"="Bearer $Token"} -Uri $MetadataURL/system/cert-bundle
$converted = ConvertFrom-Json $bundle
foreach ($i in $converted.root_certificates.psobject.Properties){
$data = [System.Convert]::FromBase64String($i.Value)
Import-Certificate -CertificateData $data -StoreName Root -StoreLocation LocalMachine
}
Update-GarmStatus -CallbackURL $CallbackURL -Message "downloading tools from $DownloadURL"
$downloadToken="{{.TempDownloadToken}}"
$DownloadTokenHeaders=@{}
if ($downloadToken.Length -gt 0) {
$DownloadTokenHeaders=@{
"Authorization"="Bearer $downloadToken"
}
}
$downloadPath = Join-Path $env:TMP {{.FileName}}
Invoke-FastWebRequest -Uri $DownloadURL -OutFile $downloadPath -Headers $DownloadTokenHeaders
$runnerDir = "C:\runner"
mkdir $runnerDir
Update-GarmStatus -CallbackURL $CallbackURL -Message "extracting runner"
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory($downloadPath, "$runnerDir")
Update-GarmStatus -CallbackURL $CallbackURL -Message "configuring and starting runner"
cd $runnerDir
{{- if .UseJITConfig }}
Update-GarmStatus -CallbackURL $CallbackURL -Message "downloading JIT credentials"
wget -UseBasicParsing -Headers @{"Accept"="application/json"; "Authorization"="Bearer $Token"} -Uri $MetadataURL/credentials/runner -OutFile (Join-Path $runnerDir ".runner")
wget -UseBasicParsing -Headers @{"Accept"="application/json"; "Authorization"="Bearer $Token"} -Uri $MetadataURL/credentials/credentials -OutFile (Join-Path $runnerDir ".credentials")
Add-Type -AssemblyName System.Security
$rsaData = (wget -UseBasicParsing -Headers @{"Accept"="application/json"; "Authorization"="Bearer $Token"} -Uri $MetadataURL/credentials/credentials_rsaparams)
$encodedBytes = [System.Text.Encoding]::UTF8.GetBytes($rsaData)
$protectedBytes = [Security.Cryptography.ProtectedData]::Protect( $encodedBytes, $null, [Security.Cryptography.DataProtectionScope]::LocalMachine )
[System.IO.File]::WriteAllBytes((Join-Path $runnerDir ".credentials_rsaparams"), $protectedBytes)
$serviceNameFile = (Join-Path $runnerDir ".service")
wget -UseBasicParsing -Headers @{"Accept"="application/json"; "Authorization"="Bearer $Token"} -Uri $MetadataURL/system/service-name -OutFile $serviceNameFile
Update-GarmStatus -CallbackURL $CallbackURL -Message "Creating system service"
$SVC_NAME=(gc -raw $serviceNameFile)
New-Service -Name "$SVC_NAME" -BinaryPathName "C:\runner\bin\RunnerService.exe" -DisplayName "$SVC_NAME" -Description "GitHub Actions Runner ($SVC_NAME)" -StartupType Automatic
Start-Service "$SVC_NAME"
Update-GarmStatus -Message "runner successfully installed" -CallbackURL $CallbackURL -Status "idle" | Out-Null
{{- else }}
$GithubRegistrationToken = Invoke-WebRequest -UseBasicParsing -Headers @{"Accept"="application/json"; "Authorization"="Bearer $Token"} -Uri $MetadataURL/runner-registration-token/
{{- if .GitHubRunnerGroup }}
./config.cmd --unattended --url "{{ .RepoURL }}" --token $GithubRegistrationToken --runnergroup {{.GitHubRunnerGroup}} --name "{{ .RunnerName }}" --labels "{{ .RunnerLabels }}" --ephemeral --runasservice
{{- else}}
./config.cmd --unattended --url "{{ .RepoURL }}" --token $GithubRegistrationToken --name "{{ .RunnerName }}" --labels "{{ .RunnerLabels }}" --ephemeral --runasservice
{{- end}}
$agentInfoFile = Join-Path $runnerDir ".runner"
$agentInfo = ConvertFrom-Json (gc -raw $agentInfoFile)
Invoke-GarmSuccess -CallbackURL $CallbackURL -Message "runner successfully installed" -AgentID $agentInfo.agentId
{{- end }}
} catch {
Invoke-GarmFailure -CallbackURL $CallbackURL -Message $_
}
}
Install-Runner
`
// InstallRunnerParams holds the parameters needed to render the runner install script.
type InstallRunnerParams struct {
// FileName is the name of the file that will be downloaded from the download URL.
// This will be the runner archive downloaded from GitHub.
FileName string
// DownloadURL is the URL from which the runner archive will be downloaded.
DownloadURL string
// RunnerUsername is the username of the user that will run the runner service.
RunnerUsername string
// RunnerGroup is the group of the user that will run the runner service.
RunnerGroup string
// RepoURL is the URL or the github repo the github runner agent needs to configure itself.
RepoURL string
// MetadataURL is the URL where instances can fetch information needed to set themselves up.
// This URL is set in the GARM config file.
MetadataURL string
// RunnerName is the name of the runner. GARM will use this to register the runner with GitHub.
RunnerName string
// RunnerLabels is a comma separated list of labels that will be added to the runner.
RunnerLabels string
// CallbackURL is the URL where the instance can send a post, signaling progress or status.
// This URL is set in the GARM config file.
CallbackURL string
// CallbackToken is the token that needs to be set by the instance in the headers in order to call
// the CallbackURL.
CallbackToken string
// TempDownloadToken is the token that needs to be set by the instance in the headers in order to download
// the githun runner. This is usually needed when using garm against a GHES instance.
TempDownloadToken string
// CABundle is a CA certificate bundle which will be sent to instances and which will tipically be installed
// as a system wide trusted root CA by either cloud-init or whatever mechanism the provider will use to set
// up the runner.
CABundle string
// GitHubRunnerGroup is the github runner group in which the newly installed runner should be added to.
GitHubRunnerGroup string
// EnableBootDebug will enable bash debug mode.
EnableBootDebug bool
// ExtraContext is a map of extra context that will be passed to the runner install template.
// This option is useful for situations in which you're supplying your own template and you need
// to pass in information that is not available in the default template.
ExtraContext map[string]string
// UseJITConfig indicates whether to attempt to configure the runner using JIT or a registration token.
UseJITConfig bool
}
func InstallRunnerScript(installParams InstallRunnerParams, osType params.OSType, tpl string) ([]byte, error) {
if tpl == "" {
switch osType {
case params.Linux:
tpl = CloudConfigTemplate
case params.Windows:
tpl = WindowsSetupScriptTemplate
default:
return nil, fmt.Errorf("unsupported os type: %s", osType)
}
}
t, err := template.New("").Parse(tpl)
if err != nil {
return nil, errors.Wrap(err, "parsing template")
}
var buf bytes.Buffer
if err := t.Execute(&buf, installParams); err != nil {
return nil, errors.Wrap(err, "rendering template")
}
return buf.Bytes(), nil
}

View file

@ -1,207 +0,0 @@
// Copyright 2023 Cloudbase Solutions SRL
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package cloudconfig
import (
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/cloudbase/garm-provider-common/defaults"
"github.com/cloudbase/garm-provider-common/params"
"github.com/pkg/errors"
)
// CloudConfigSpec is a struct that holds extra specs that can be used to customize user data.
type CloudConfigSpec struct {
// RunnerInstallTemplate can be used to override the default runner install template.
// If used, the caller is responsible for the correctness of the template as well as the
// suitability of the template for the target OS.
RunnerInstallTemplate []byte `json:"runner_install_template"`
// PreInstallScripts is a map of pre-install scripts that will be run before the
// runner install script. These will run as root and can be used to prep a generic image
// before we attempt to install the runner. The key of the map is the name of the script
// as it will be written to disk. The value is a byte array with the contents of the script.
//
// These scripts will be added and run in alphabetical order.
//
// On Linux, we will set the executable flag. On Windows, the name matters as Windows looks for an
// extension to determine if the file is an executable or not. In theory this can hold binaries,
// but in most cases this will most likely hold scripts. We do not currenly validate the payload,
// so it's up to the user what they upload here.
// Caution needs to be exercised when using this feature, as the total size of userdata is limited
// on most providers.
PreInstallScripts map[string][]byte `json:"pre_install_scripts"`
// ExtraContext is a map of extra context that will be passed to the runner install template.
ExtraContext map[string]string `json:"extra_context"`
}
func sortMapKeys(m map[string][]byte) []string {
var keys []string
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
// GetSpecs returns the cloud config specific extra specs from the bootstrap params.
func GetSpecs(bootstrapParams params.BootstrapInstance) (CloudConfigSpec, error) {
var extraSpecs CloudConfigSpec
if len(bootstrapParams.ExtraSpecs) == 0 {
return extraSpecs, nil
}
if err := json.Unmarshal(bootstrapParams.ExtraSpecs, &extraSpecs); err != nil {
return CloudConfigSpec{}, errors.Wrap(err, "unmarshaling extra specs")
}
if extraSpecs.ExtraContext == nil {
extraSpecs.ExtraContext = map[string]string{}
}
if extraSpecs.PreInstallScripts == nil {
extraSpecs.PreInstallScripts = map[string][]byte{}
}
return extraSpecs, nil
}
// GetRunnerInstallScript returns the runner install script for the given bootstrap params.
// This function will return either the default script for the given OS type or will use the supplied template
// if one is provided.
func GetRunnerInstallScript(bootstrapParams params.BootstrapInstance, tools params.RunnerApplicationDownload, runnerName string) ([]byte, error) {
if tools.GetFilename() == "" {
return nil, fmt.Errorf("missing tools filename")
}
if tools.GetDownloadURL() == "" {
return nil, fmt.Errorf("missing tools download URL")
}
tempToken := tools.GetTempDownloadToken()
extraSpecs, err := GetSpecs(bootstrapParams)
if err != nil {
return nil, errors.Wrap(err, "getting specs")
}
installRunnerParams := InstallRunnerParams{
FileName: tools.GetFilename(),
DownloadURL: tools.GetDownloadURL(),
TempDownloadToken: tempToken,
MetadataURL: bootstrapParams.MetadataURL,
RunnerUsername: defaults.DefaultUser,
RunnerGroup: defaults.DefaultUser,
RepoURL: bootstrapParams.RepoURL,
RunnerName: runnerName,
RunnerLabels: strings.Join(bootstrapParams.Labels, ","),
CallbackURL: bootstrapParams.CallbackURL,
CallbackToken: bootstrapParams.InstanceToken,
GitHubRunnerGroup: bootstrapParams.GitHubRunnerGroup,
ExtraContext: extraSpecs.ExtraContext,
EnableBootDebug: bootstrapParams.UserDataOptions.EnableBootDebug,
UseJITConfig: bootstrapParams.JitConfigEnabled,
}
if bootstrapParams.CACertBundle != nil && len(bootstrapParams.CACertBundle) > 0 {
installRunnerParams.CABundle = string(bootstrapParams.CACertBundle)
}
installScript, err := InstallRunnerScript(installRunnerParams, bootstrapParams.OSType, string(extraSpecs.RunnerInstallTemplate))
if err != nil {
return nil, errors.Wrap(err, "generating script")
}
return installScript, nil
}
// GetCloudInitConfig returns the cloud-init specific userdata config. This config can be used on most clouds
// for most Linux machines. The install runner script must be generated separately either by GetRunnerInstallScript()
// or some other means.
func GetCloudInitConfig(bootstrapParams params.BootstrapInstance, installScript []byte) (string, error) {
extraSpecs, err := GetSpecs(bootstrapParams)
if err != nil {
return "", errors.Wrap(err, "getting specs")
}
cloudCfg := NewDefaultCloudInitConfig()
if bootstrapParams.UserDataOptions.DisableUpdatesOnBoot {
cloudCfg.PackageUpgrade = false
cloudCfg.Packages = []string{}
}
for _, pkg := range bootstrapParams.UserDataOptions.ExtraPackages {
cloudCfg.AddPackage(pkg)
}
if len(extraSpecs.PreInstallScripts) > 0 {
names := sortMapKeys(extraSpecs.PreInstallScripts)
for _, name := range names {
script := extraSpecs.PreInstallScripts[name]
cloudCfg.AddFile(script, fmt.Sprintf("/garm-pre-install/%s", name), "root:root", "755")
cloudCfg.AddRunCmd(fmt.Sprintf("/garm-pre-install/%s", name))
}
}
cloudCfg.AddRunCmd("rm -rf /garm-pre-install")
cloudCfg.AddSSHKey(bootstrapParams.SSHKeys...)
cloudCfg.AddFile(installScript, "/install_runner.sh", "root:root", "755")
cloudCfg.AddRunCmd(fmt.Sprintf("su -l -c /install_runner.sh %s", defaults.DefaultUser))
cloudCfg.AddRunCmd("rm -f /install_runner.sh")
if bootstrapParams.CACertBundle != nil && len(bootstrapParams.CACertBundle) > 0 {
if err := cloudCfg.AddCACert(bootstrapParams.CACertBundle); err != nil {
return "", errors.Wrap(err, "adding CA cert bundle")
}
}
asStr, err := cloudCfg.Serialize()
if err != nil {
return "", errors.Wrap(err, "creating cloud config")
}
return asStr, nil
}
// GetCloudConfig is a helper function that generates a cloud-init config for Linux and a powershell script for Windows.
// In most cases this function should do, but in situations where a more custom approach is needed, you may need to call
// GetCloudInitConfig() or GetRunnerInstallScript() directly and compose the final userdata in a different way.
// The extra specs PreInstallScripts is only supported on Linux via cloud-init by this function. On some providers, like Azure
// Windows initialization scripts are run by creating a separate CustomScriptExtension resource for each individual script.
// On other clouds it may be different. This function aims to be generic, which is why it only supports the PreInstallScripts
// via cloud-init.
func GetCloudConfig(bootstrapParams params.BootstrapInstance, tools params.RunnerApplicationDownload, runnerName string) (string, error) {
installScript, err := GetRunnerInstallScript(bootstrapParams, tools, runnerName)
if err != nil {
return "", errors.Wrap(err, "generating script")
}
var asStr string
switch bootstrapParams.OSType {
case params.Linux:
cloudCfg, err := GetCloudInitConfig(bootstrapParams, installScript)
if err != nil {
return "", errors.Wrap(err, "getting cloud init config")
}
return cloudCfg, nil
case params.Windows:
asStr = string(installScript)
default:
return "", fmt.Errorf("unknown os type: %s", bootstrapParams.OSType)
}
return asStr, nil
}

View file

@ -1 +0,0 @@
* text eol=lf

View file

@ -1,41 +0,0 @@
# 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

View file

@ -1,8 +0,0 @@
language: go
os:
- linux
- osx
go:
- 1.12
script:
- go test -v

View file

@ -1,11 +0,0 @@
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.

View file

@ -1,20 +0,0 @@
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.

View file

@ -1,167 +0,0 @@
# [pongo](https://en.wikipedia.org/wiki/Pongo_%28genus%29)2
[![PkgGoDev](https://pkg.go.dev/badge/flosch/pongo2)](https://pkg.go.dev/flosch/pongo2)
[![Build Status](https://travis-ci.org/flosch/pongo2.svg?branch=master)](https://travis-ci.org/flosch/pongo2)
pongo2 is a Django-syntax like templating-language.
Install/update using `go get` (no dependencies required by pongo2):
```sh
go get -u github.com/flosch/pongo2
```
Please use the [issue tracker](https://github.com/flosch/pongo2/issues) if you're encountering any problems with pongo2 or if you need help with implementing tags or filters ([create a ticket!](https://github.com/flosch/pongo2/issues/new)).
## First impression of a template
```django
<html>
<head>
<title>Our admins and users</title>
</head>
{# This is a short example to give you a quick overview of pongo2's syntax. #}
{% macro user_details(user, is_admin=false) %}
<div class="user_item">
<!-- Let's indicate a user's good karma -->
<h2 {% if (user.karma>
= 40) || (user.karma > calc_avg_karma(userlist)+5) %} class="karma-good"{%
endif %}>
<!-- This will call user.String() automatically if available: -->
{{ user }}
</h2>
<!-- Will print a human-readable time duration like "3 weeks ago" -->
<p>This user registered {{ user.register_date|naturaltime }}.</p>
<!-- Let's allow the users to write down their biography using markdown;
we will only show the first 15 words as a preview -->
<p>The user's biography:</p>
<p>
{{ user.biography|markdown|truncatewords_html:15 }}
<a href="/user/{{ user.id }}/">read more</a>
</p>
{% if is_admin %}
<p>This user is an admin!</p>
{% endif %}
</div>
{% endmacro %}
<body>
<!-- Make use of the macro defined above to avoid repetitive HTML code
since we want to use the same code for admins AND members -->
<h1>Our admins</h1>
{% for admin in adminlist %} {{ user_details(admin, true) }} {% endfor %}
<h1>Our members</h1>
{% for user in userlist %} {{ user_details(user) }} {% endfor %}
</body>
</html>
```
## Features
- Syntax- and feature-set-compatible with [Django 1.7](https://django.readthedocs.io/en/1.7.x/topics/templates.html)
- [Advanced C-like expressions](https://github.com/flosch/pongo2/blob/master/template_tests/expressions.tpl).
- [Complex function calls within expressions](https://github.com/flosch/pongo2/blob/master/template_tests/function_calls_wrapper.tpl).
- [Easy API to create new filters and tags](http://godoc.org/github.com/flosch/pongo2#RegisterFilter) ([including parsing arguments](http://godoc.org/github.com/flosch/pongo2#Parser))
- Additional features:
- Macros including importing macros from other files (see [template_tests/macro.tpl](https://github.com/flosch/pongo2/blob/master/template_tests/macro.tpl))
- [Template sandboxing](https://godoc.org/github.com/flosch/pongo2#TemplateSet) ([directory patterns](http://golang.org/pkg/path/filepath/#Match), banned tags/filters)
## Caveats
### Filters
- **date** / **time**: The `date` and `time` filter are taking the Golang specific time- and date-format (not Django's one) currently. [Take a look on the format here](http://golang.org/pkg/time/#Time.Format).
- **stringformat**: `stringformat` does **not** take Python's string format syntax as a parameter, instead it takes Go's. Essentially `{{ 3.14|stringformat:"pi is %.2f" }}` is `fmt.Sprintf("pi is %.2f", 3.14)`.
- **escape** / **force_escape**: Unlike Django's behaviour, the `escape`-filter is applied immediately. Therefore there is no need for a `force_escape`-filter yet.
### Tags
- **for**: All the `forloop` fields (like `forloop.counter`) are written with a capital letter at the beginning. For example, the `counter` can be accessed by `forloop.Counter` and the parentloop by `forloop.Parentloop`.
- **now**: takes Go's time format (see **date** and **time**-filter).
### Misc
- **not in-operator**: You can check whether a map/struct/string contains a key/field/substring by using the in-operator (or the negation of it):
`{% if key in map %}Key is in map{% else %}Key not in map{% endif %}` or `{% if !(key in map) %}Key is NOT in map{% else %}Key is in map{% endif %}`.
## Add-ons, libraries and helpers
### Official
- [pongo2-addons](https://github.com/flosch/pongo2-addons) - Official additional filters/tags for pongo2 (for example a **markdown**-filter). They are in their own repository because they're relying on 3rd-party-libraries.
### 3rd-party
- [beego-pongo2](https://github.com/oal/beego-pongo2) - A tiny little helper for using Pongo2 with [Beego](https://github.com/astaxie/beego).
- [beego-pongo2.v2](https://github.com/ipfans/beego-pongo2.v2) - Same as `beego-pongo2`, but for pongo2 v2.
- [macaron-pongo2](https://github.com/macaron-contrib/pongo2) - pongo2 support for [Macaron](https://github.com/Unknwon/macaron), a modular web framework.
- [ginpongo2](https://github.com/ngerakines/ginpongo2) - middleware for [gin](github.com/gin-gonic/gin) to use pongo2 templates
- [Build'n support for Iris' template engine](https://github.com/kataras/iris)
- [pongo2gin](https://gitlab.com/go-box/pongo2gin) - alternative renderer for [gin](github.com/gin-gonic/gin) to use pongo2 templates
- [pongo2-trans](https://github.com/digitalcrab/pongo2trans) - `trans`-tag implementation for internationalization
- [tpongo2](https://github.com/tango-contrib/tpongo2) - pongo2 support for [Tango](https://github.com/lunny/tango), a micro-kernel & pluggable web framework.
- [p2cli](https://github.com/wrouesnel/p2cli) - command line templating utility based on pongo2
Please add your project to this list and send me a pull request when you've developed something nice for pongo2.
## Who's using pongo2
[I'm compiling a list of pongo2 users](https://github.com/flosch/pongo2/issues/241). Add your project or company!
## API-usage examples
Please see the documentation for a full list of provided API methods.
### A tiny example (template string)
```go
// 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": "florian"})
if err != nil {
panic(err)
}
fmt.Println(out) // Output: Hello Florian!
```
## Example server-usage (template file)
```go
package main
import (
"github.com/flosch/pongo2"
"net/http"
)
// Pre-compiling the templates at application startup using the
// little Must()-helper function (Must() will panic if FromFile()
// or FromString() will return with an error - that's it).
// It's faster to pre-compile it anywhere at startup and only
// execute the template later.
var tplExample = pongo2.Must(pongo2.FromFile("example.html"))
func examplePage(w http.ResponseWriter, r *http.Request) {
// Execute the template per HTTP request
err := tplExample.ExecuteWriter(pongo2.Context{"query": r.FormValue("query")}, w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func main() {
http.HandleFunc("/", examplePage)
http.ListenAndServe(":8080", nil)
}
```

View file

@ -1,137 +0,0 @@
package pongo2
import (
"fmt"
"regexp"
"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: fmt.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...)
}

View file

@ -1,31 +0,0 @@
// 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

View file

@ -1,91 +0,0 @@
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
}

View file

@ -1,141 +0,0 @@
package pongo2
import (
"fmt"
)
// 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 fmt.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 fmt.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: fmt.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
}

View file

@ -1,927 +0,0 @@
package pongo2
/* Filters that are provided through github.com/flosch/pongo2-addons:
------------------------------------------------------------------
filesizeformat
slugify
timesince
timeuntil
Filters that won't be added:
----------------------------
get_static_prefix (reason: web-framework specific)
pprint (reason: python-specific)
static (reason: web-framework specific)
Reconsideration (not implemented yet):
--------------------------------------
force_escape (reason: not yet needed since this is the behaviour of pongo2's escape filter)
safeseq (reason: same reason as `force_escape`)
unordered_list (python-specific; not sure whether needed or not)
dictsort (python-specific; maybe one could add a filter to sort a list of structs by a specific field name)
dictsortreversed (see dictsort)
*/
import (
"bytes"
"fmt"
"math/rand"
"net/url"
"regexp"
"strconv"
"strings"
"time"
"unicode/utf8"
"errors"
)
func init() {
rand.Seed(time.Now().Unix())
RegisterFilter("escape", filterEscape)
RegisterFilter("safe", filterSafe)
RegisterFilter("escapejs", filterEscapejs)
RegisterFilter("add", filterAdd)
RegisterFilter("addslashes", filterAddslashes)
RegisterFilter("capfirst", filterCapfirst)
RegisterFilter("center", filterCenter)
RegisterFilter("cut", filterCut)
RegisterFilter("date", filterDate)
RegisterFilter("default", filterDefault)
RegisterFilter("default_if_none", filterDefaultIfNone)
RegisterFilter("divisibleby", filterDivisibleby)
RegisterFilter("first", filterFirst)
RegisterFilter("floatformat", filterFloatformat)
RegisterFilter("get_digit", filterGetdigit)
RegisterFilter("iriencode", filterIriencode)
RegisterFilter("join", filterJoin)
RegisterFilter("last", filterLast)
RegisterFilter("length", filterLength)
RegisterFilter("length_is", filterLengthis)
RegisterFilter("linebreaks", filterLinebreaks)
RegisterFilter("linebreaksbr", filterLinebreaksbr)
RegisterFilter("linenumbers", filterLinenumbers)
RegisterFilter("ljust", filterLjust)
RegisterFilter("lower", filterLower)
RegisterFilter("make_list", filterMakelist)
RegisterFilter("phone2numeric", filterPhone2numeric)
RegisterFilter("pluralize", filterPluralize)
RegisterFilter("random", filterRandom)
RegisterFilter("removetags", filterRemovetags)
RegisterFilter("rjust", filterRjust)
RegisterFilter("slice", filterSlice)
RegisterFilter("split", filterSplit)
RegisterFilter("stringformat", filterStringformat)
RegisterFilter("striptags", filterStriptags)
RegisterFilter("time", filterDate) // time uses filterDate (same golang-format)
RegisterFilter("title", filterTitle)
RegisterFilter("truncatechars", filterTruncatechars)
RegisterFilter("truncatechars_html", filterTruncatecharsHTML)
RegisterFilter("truncatewords", filterTruncatewords)
RegisterFilter("truncatewords_html", filterTruncatewordsHTML)
RegisterFilter("upper", filterUpper)
RegisterFilter("urlencode", filterUrlencode)
RegisterFilter("urlize", filterUrlize)
RegisterFilter("urlizetrunc", filterUrlizetrunc)
RegisterFilter("wordcount", filterWordcount)
RegisterFilter("wordwrap", filterWordwrap)
RegisterFilter("yesno", filterYesno)
RegisterFilter("float", filterFloat) // pongo-specific
RegisterFilter("integer", filterInteger) // pongo-specific
}
func filterTruncatecharsHelper(s string, newLen int) string {
runes := []rune(s)
if newLen < len(runes) {
if newLen >= 3 {
return fmt.Sprintf("%s...", string(runes[:newLen-3]))
}
// Not enough space for the ellipsis
return string(runes[:newLen])
}
return string(runes)
}
func filterTruncateHTMLHelper(value string, newOutput *bytes.Buffer, cond func() bool, fn func(c rune, s int, idx int) int, finalize func()) {
vLen := len(value)
var tagStack []string
idx := 0
for idx < vLen && !cond() {
c, s := utf8.DecodeRuneInString(value[idx:])
if c == utf8.RuneError {
idx += s
continue
}
if c == '<' {
newOutput.WriteRune(c)
idx += s // consume "<"
if idx+1 < vLen {
if value[idx] == '/' {
// Close tag
newOutput.WriteString("/")
tag := ""
idx++ // consume "/"
for idx < vLen {
c2, size2 := utf8.DecodeRuneInString(value[idx:])
if c2 == utf8.RuneError {
idx += size2
continue
}
// End of tag found
if c2 == '>' {
idx++ // consume ">"
break
}
tag += string(c2)
idx += size2
}
if len(tagStack) > 0 {
// Ideally, the close tag is TOP of tag stack
// In malformed HTML, it must not be, so iterate through the stack and remove the tag
for i := len(tagStack) - 1; i >= 0; i-- {
if tagStack[i] == tag {
// Found the tag
tagStack[i] = tagStack[len(tagStack)-1]
tagStack = tagStack[:len(tagStack)-1]
break
}
}
}
newOutput.WriteString(tag)
newOutput.WriteString(">")
} else {
// Open tag
tag := ""
params := false
for idx < vLen {
c2, size2 := utf8.DecodeRuneInString(value[idx:])
if c2 == utf8.RuneError {
idx += size2
continue
}
newOutput.WriteRune(c2)
// End of tag found
if c2 == '>' {
idx++ // consume ">"
break
}
if !params {
if c2 == ' ' {
params = true
} else {
tag += string(c2)
}
}
idx += size2
}
// Add tag to stack
tagStack = append(tagStack, tag)
}
}
} else {
idx = fn(c, s, idx)
}
}
finalize()
for i := len(tagStack) - 1; i >= 0; i-- {
tag := tagStack[i]
// Close everything from the regular tag stack
newOutput.WriteString(fmt.Sprintf("</%s>", tag))
}
}
func filterTruncatechars(in *Value, param *Value) (*Value, *Error) {
s := in.String()
newLen := param.Integer()
return AsValue(filterTruncatecharsHelper(s, newLen)), nil
}
func filterTruncatecharsHTML(in *Value, param *Value) (*Value, *Error) {
value := in.String()
newLen := max(param.Integer()-3, 0)
newOutput := bytes.NewBuffer(nil)
textcounter := 0
filterTruncateHTMLHelper(value, newOutput, func() bool {
return textcounter >= newLen
}, func(c rune, s int, idx int) int {
textcounter++
newOutput.WriteRune(c)
return idx + s
}, func() {
if textcounter >= newLen && textcounter < len(value) {
newOutput.WriteString("...")
}
})
return AsSafeValue(newOutput.String()), nil
}
func filterTruncatewords(in *Value, param *Value) (*Value, *Error) {
words := strings.Fields(in.String())
n := param.Integer()
if n <= 0 {
return AsValue(""), nil
}
nlen := min(len(words), n)
out := make([]string, 0, nlen)
for i := 0; i < nlen; i++ {
out = append(out, words[i])
}
if n < len(words) {
out = append(out, "...")
}
return AsValue(strings.Join(out, " ")), nil
}
func filterTruncatewordsHTML(in *Value, param *Value) (*Value, *Error) {
value := in.String()
newLen := max(param.Integer(), 0)
newOutput := bytes.NewBuffer(nil)
wordcounter := 0
filterTruncateHTMLHelper(value, newOutput, func() bool {
return wordcounter >= newLen
}, func(_ rune, _ int, idx int) int {
// Get next word
wordFound := false
for idx < len(value) {
c2, size2 := utf8.DecodeRuneInString(value[idx:])
if c2 == utf8.RuneError {
idx += size2
continue
}
if c2 == '<' {
// HTML tag start, don't consume it
return idx
}
newOutput.WriteRune(c2)
idx += size2
if c2 == ' ' || c2 == '.' || c2 == ',' || c2 == ';' {
// Word ends here, stop capturing it now
break
} else {
wordFound = true
}
}
if wordFound {
wordcounter++
}
return idx
}, func() {
if wordcounter >= newLen {
newOutput.WriteString("...")
}
})
return AsSafeValue(newOutput.String()), nil
}
func filterEscape(in *Value, param *Value) (*Value, *Error) {
output := strings.Replace(in.String(), "&", "&amp;", -1)
output = strings.Replace(output, ">", "&gt;", -1)
output = strings.Replace(output, "<", "&lt;", -1)
output = strings.Replace(output, "\"", "&quot;", -1)
output = strings.Replace(output, "'", "&#39;", -1)
return AsValue(output), nil
}
func filterSafe(in *Value, param *Value) (*Value, *Error) {
return in, nil // nothing to do here, just to keep track of the safe application
}
func filterEscapejs(in *Value, param *Value) (*Value, *Error) {
sin := in.String()
var b bytes.Buffer
idx := 0
for idx < len(sin) {
c, size := utf8.DecodeRuneInString(sin[idx:])
if c == utf8.RuneError {
idx += size
continue
}
if c == '\\' {
// Escape seq?
if idx+1 < len(sin) {
switch sin[idx+1] {
case 'r':
b.WriteString(fmt.Sprintf(`\u%04X`, '\r'))
idx += 2
continue
case 'n':
b.WriteString(fmt.Sprintf(`\u%04X`, '\n'))
idx += 2
continue
/*case '\'':
b.WriteString(fmt.Sprintf(`\u%04X`, '\''))
idx += 2
continue
case '"':
b.WriteString(fmt.Sprintf(`\u%04X`, '"'))
idx += 2
continue*/
}
}
}
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == ' ' || c == '/' {
b.WriteRune(c)
} else {
b.WriteString(fmt.Sprintf(`\u%04X`, c))
}
idx += size
}
return AsValue(b.String()), nil
}
func filterAdd(in *Value, param *Value) (*Value, *Error) {
if in.IsNumber() && param.IsNumber() {
if in.IsFloat() || param.IsFloat() {
return AsValue(in.Float() + param.Float()), nil
}
return AsValue(in.Integer() + param.Integer()), nil
}
// If in/param is not a number, we're relying on the
// Value's String() conversion and just add them both together
return AsValue(in.String() + param.String()), nil
}
func filterAddslashes(in *Value, param *Value) (*Value, *Error) {
output := strings.Replace(in.String(), "\\", "\\\\", -1)
output = strings.Replace(output, "\"", "\\\"", -1)
output = strings.Replace(output, "'", "\\'", -1)
return AsValue(output), nil
}
func filterCut(in *Value, param *Value) (*Value, *Error) {
return AsValue(strings.Replace(in.String(), param.String(), "", -1)), nil
}
func filterLength(in *Value, param *Value) (*Value, *Error) {
return AsValue(in.Len()), nil
}
func filterLengthis(in *Value, param *Value) (*Value, *Error) {
return AsValue(in.Len() == param.Integer()), nil
}
func filterDefault(in *Value, param *Value) (*Value, *Error) {
if !in.IsTrue() {
return param, nil
}
return in, nil
}
func filterDefaultIfNone(in *Value, param *Value) (*Value, *Error) {
if in.IsNil() {
return param, nil
}
return in, nil
}
func filterDivisibleby(in *Value, param *Value) (*Value, *Error) {
if param.Integer() == 0 {
return AsValue(false), nil
}
return AsValue(in.Integer()%param.Integer() == 0), nil
}
func filterFirst(in *Value, param *Value) (*Value, *Error) {
if in.CanSlice() && in.Len() > 0 {
return in.Index(0), nil
}
return AsValue(""), nil
}
func filterFloatformat(in *Value, param *Value) (*Value, *Error) {
val := in.Float()
decimals := -1
if !param.IsNil() {
// Any argument provided?
decimals = param.Integer()
}
// if the argument is not a number (e. g. empty), the default
// behaviour is trim the result
trim := !param.IsNumber()
if decimals <= 0 {
// argument is negative or zero, so we
// want the output being trimmed
decimals = -decimals
trim = true
}
if trim {
// Remove zeroes
if float64(int(val)) == val {
return AsValue(in.Integer()), nil
}
}
return AsValue(strconv.FormatFloat(val, 'f', decimals, 64)), nil
}
func filterGetdigit(in *Value, param *Value) (*Value, *Error) {
i := param.Integer()
l := len(in.String()) // do NOT use in.Len() here!
if i <= 0 || i > l {
return in, nil
}
return AsValue(in.String()[l-i] - 48), nil
}
const filterIRIChars = "/#%[]=:;$&()+,!?*@'~"
func filterIriencode(in *Value, param *Value) (*Value, *Error) {
var b bytes.Buffer
sin := in.String()
for _, r := range sin {
if strings.IndexRune(filterIRIChars, r) >= 0 {
b.WriteRune(r)
} else {
b.WriteString(url.QueryEscape(string(r)))
}
}
return AsValue(b.String()), nil
}
func filterJoin(in *Value, param *Value) (*Value, *Error) {
if !in.CanSlice() {
return in, nil
}
sep := param.String()
sl := make([]string, 0, in.Len())
for i := 0; i < in.Len(); i++ {
sl = append(sl, in.Index(i).String())
}
return AsValue(strings.Join(sl, sep)), nil
}
func filterLast(in *Value, param *Value) (*Value, *Error) {
if in.CanSlice() && in.Len() > 0 {
return in.Index(in.Len() - 1), nil
}
return AsValue(""), nil
}
func filterUpper(in *Value, param *Value) (*Value, *Error) {
return AsValue(strings.ToUpper(in.String())), nil
}
func filterLower(in *Value, param *Value) (*Value, *Error) {
return AsValue(strings.ToLower(in.String())), nil
}
func filterMakelist(in *Value, param *Value) (*Value, *Error) {
s := in.String()
result := make([]string, 0, len(s))
for _, c := range s {
result = append(result, string(c))
}
return AsValue(result), nil
}
func filterCapfirst(in *Value, param *Value) (*Value, *Error) {
if in.Len() <= 0 {
return AsValue(""), nil
}
t := in.String()
r, size := utf8.DecodeRuneInString(t)
return AsValue(strings.ToUpper(string(r)) + t[size:]), nil
}
func filterCenter(in *Value, param *Value) (*Value, *Error) {
width := param.Integer()
slen := in.Len()
if width <= slen {
return in, nil
}
spaces := width - slen
left := spaces/2 + spaces%2
right := spaces / 2
return AsValue(fmt.Sprintf("%s%s%s", strings.Repeat(" ", left),
in.String(), strings.Repeat(" ", right))), nil
}
func filterDate(in *Value, param *Value) (*Value, *Error) {
t, isTime := in.Interface().(time.Time)
if !isTime {
return nil, &Error{
Sender: "filter:date",
OrigError: errors.New("filter input argument must be of type 'time.Time'"),
}
}
return AsValue(t.Format(param.String())), nil
}
func filterFloat(in *Value, param *Value) (*Value, *Error) {
return AsValue(in.Float()), nil
}
func filterInteger(in *Value, param *Value) (*Value, *Error) {
return AsValue(in.Integer()), nil
}
func filterLinebreaks(in *Value, param *Value) (*Value, *Error) {
if in.Len() == 0 {
return in, nil
}
var b bytes.Buffer
// Newline = <br />
// Double newline = <p>...</p>
lines := strings.Split(in.String(), "\n")
lenlines := len(lines)
opened := false
for idx, line := range lines {
if !opened {
b.WriteString("<p>")
opened = true
}
b.WriteString(line)
if idx < lenlines-1 && strings.TrimSpace(lines[idx]) != "" {
// We've not reached the end
if strings.TrimSpace(lines[idx+1]) == "" {
// Next line is empty
if opened {
b.WriteString("</p>")
opened = false
}
} else {
b.WriteString("<br />")
}
}
}
if opened {
b.WriteString("</p>")
}
return AsValue(b.String()), nil
}
func filterSplit(in *Value, param *Value) (*Value, *Error) {
chunks := strings.Split(in.String(), param.String())
return AsValue(chunks), nil
}
func filterLinebreaksbr(in *Value, param *Value) (*Value, *Error) {
return AsValue(strings.Replace(in.String(), "\n", "<br />", -1)), nil
}
func filterLinenumbers(in *Value, param *Value) (*Value, *Error) {
lines := strings.Split(in.String(), "\n")
output := make([]string, 0, len(lines))
for idx, line := range lines {
output = append(output, fmt.Sprintf("%d. %s", idx+1, line))
}
return AsValue(strings.Join(output, "\n")), nil
}
func filterLjust(in *Value, param *Value) (*Value, *Error) {
times := param.Integer() - in.Len()
if times < 0 {
times = 0
}
return AsValue(fmt.Sprintf("%s%s", in.String(), strings.Repeat(" ", times))), nil
}
func filterUrlencode(in *Value, param *Value) (*Value, *Error) {
return AsValue(url.QueryEscape(in.String())), nil
}
// TODO: This regexp could do some work
var filterUrlizeURLRegexp = regexp.MustCompile(`((((http|https)://)|www\.|((^|[ ])[0-9A-Za-z_\-]+(\.com|\.net|\.org|\.info|\.biz|\.de))))(?U:.*)([ ]+|$)`)
var filterUrlizeEmailRegexp = regexp.MustCompile(`(\w+@\w+\.\w{2,4})`)
func filterUrlizeHelper(input string, autoescape bool, trunc int) (string, error) {
var soutErr error
sout := filterUrlizeURLRegexp.ReplaceAllStringFunc(input, func(raw_url string) string {
var prefix string
var suffix string
if strings.HasPrefix(raw_url, " ") {
prefix = " "
}
if strings.HasSuffix(raw_url, " ") {
suffix = " "
}
raw_url = strings.TrimSpace(raw_url)
t, err := ApplyFilter("iriencode", AsValue(raw_url), nil)
if err != nil {
soutErr = err
return ""
}
url := t.String()
if !strings.HasPrefix(url, "http") {
url = fmt.Sprintf("http://%s", url)
}
title := raw_url
if trunc > 3 && len(title) > trunc {
title = fmt.Sprintf("%s...", title[:trunc-3])
}
if autoescape {
t, err := ApplyFilter("escape", AsValue(title), nil)
if err != nil {
soutErr = err
return ""
}
title = t.String()
}
return fmt.Sprintf(`%s<a href="%s" rel="nofollow">%s</a>%s`, prefix, url, title, suffix)
})
if soutErr != nil {
return "", soutErr
}
sout = filterUrlizeEmailRegexp.ReplaceAllStringFunc(sout, func(mail string) string {
title := mail
if trunc > 3 && len(title) > trunc {
title = fmt.Sprintf("%s...", title[:trunc-3])
}
return fmt.Sprintf(`<a href="mailto:%s">%s</a>`, mail, title)
})
return sout, nil
}
func filterUrlize(in *Value, param *Value) (*Value, *Error) {
autoescape := true
if param.IsBool() {
autoescape = param.Bool()
}
s, err := filterUrlizeHelper(in.String(), autoescape, -1)
if err != nil {
}
return AsValue(s), nil
}
func filterUrlizetrunc(in *Value, param *Value) (*Value, *Error) {
s, err := filterUrlizeHelper(in.String(), true, param.Integer())
if err != nil {
return nil, &Error{
Sender: "filter:urlizetrunc",
OrigError: errors.New("you cannot pass more than 2 arguments to filter 'pluralize'"),
}
}
return AsValue(s), nil
}
func filterStringformat(in *Value, param *Value) (*Value, *Error) {
return AsValue(fmt.Sprintf(param.String(), in.Interface())), nil
}
var reStriptags = regexp.MustCompile("<[^>]*?>")
func filterStriptags(in *Value, param *Value) (*Value, *Error) {
s := in.String()
// Strip all tags
s = reStriptags.ReplaceAllString(s, "")
return AsValue(strings.TrimSpace(s)), nil
}
// https://en.wikipedia.org/wiki/Phoneword
var filterPhone2numericMap = map[string]string{
"a": "2", "b": "2", "c": "2", "d": "3", "e": "3", "f": "3", "g": "4", "h": "4", "i": "4", "j": "5", "k": "5",
"l": "5", "m": "6", "n": "6", "o": "6", "p": "7", "q": "7", "r": "7", "s": "7", "t": "8", "u": "8", "v": "8",
"w": "9", "x": "9", "y": "9", "z": "9",
}
func filterPhone2numeric(in *Value, param *Value) (*Value, *Error) {
sin := in.String()
for k, v := range filterPhone2numericMap {
sin = strings.Replace(sin, k, v, -1)
sin = strings.Replace(sin, strings.ToUpper(k), v, -1)
}
return AsValue(sin), nil
}
func filterPluralize(in *Value, param *Value) (*Value, *Error) {
if in.IsNumber() {
// Works only on numbers
if param.Len() > 0 {
endings := strings.Split(param.String(), ",")
if len(endings) > 2 {
return nil, &Error{
Sender: "filter:pluralize",
OrigError: errors.New("you cannot pass more than 2 arguments to filter 'pluralize'"),
}
}
if len(endings) == 1 {
// 1 argument
if in.Integer() != 1 {
return AsValue(endings[0]), nil
}
} else {
if in.Integer() != 1 {
// 2 arguments
return AsValue(endings[1]), nil
}
return AsValue(endings[0]), nil
}
} else {
if in.Integer() != 1 {
// return default 's'
return AsValue("s"), nil
}
}
return AsValue(""), nil
}
return nil, &Error{
Sender: "filter:pluralize",
OrigError: errors.New("filter 'pluralize' does only work on numbers"),
}
}
func filterRandom(in *Value, param *Value) (*Value, *Error) {
if !in.CanSlice() || in.Len() <= 0 {
return in, nil
}
i := rand.Intn(in.Len())
return in.Index(i), nil
}
func filterRemovetags(in *Value, param *Value) (*Value, *Error) {
s := in.String()
tags := strings.Split(param.String(), ",")
// Strip only specific tags
for _, tag := range tags {
re := regexp.MustCompile(fmt.Sprintf("</?%s/?>", tag))
s = re.ReplaceAllString(s, "")
}
return AsValue(strings.TrimSpace(s)), nil
}
func filterRjust(in *Value, param *Value) (*Value, *Error) {
return AsValue(fmt.Sprintf(fmt.Sprintf("%%%ds", param.Integer()), in.String())), nil
}
func filterSlice(in *Value, param *Value) (*Value, *Error) {
comp := strings.Split(param.String(), ":")
if len(comp) != 2 {
return nil, &Error{
Sender: "filter:slice",
OrigError: errors.New("Slice string must have the format 'from:to' [from/to can be omitted, but the ':' is required]"),
}
}
if !in.CanSlice() {
return in, nil
}
from := AsValue(comp[0]).Integer()
to := in.Len()
if from > to {
from = to
}
vto := AsValue(comp[1]).Integer()
if vto >= from && vto <= in.Len() {
to = vto
}
return in.Slice(from, to), nil
}
func filterTitle(in *Value, param *Value) (*Value, *Error) {
if !in.IsString() {
return AsValue(""), nil
}
return AsValue(strings.Title(strings.ToLower(in.String()))), nil
}
func filterWordcount(in *Value, param *Value) (*Value, *Error) {
return AsValue(len(strings.Fields(in.String()))), nil
}
func filterWordwrap(in *Value, param *Value) (*Value, *Error) {
words := strings.Fields(in.String())
wordsLen := len(words)
wrapAt := param.Integer()
if wrapAt <= 0 {
return in, nil
}
linecount := wordsLen/wrapAt + wordsLen%wrapAt
lines := make([]string, 0, linecount)
for i := 0; i < linecount; i++ {
lines = append(lines, strings.Join(words[wrapAt*i:min(wrapAt*(i+1), wordsLen)], " "))
}
return AsValue(strings.Join(lines, "\n")), nil
}
func filterYesno(in *Value, param *Value) (*Value, *Error) {
choices := map[int]string{
0: "yes",
1: "no",
2: "maybe",
}
paramString := param.String()
customChoices := strings.Split(paramString, ",")
if len(paramString) > 0 {
if len(customChoices) > 3 {
return nil, &Error{
Sender: "filter:yesno",
OrigError: fmt.Errorf("You cannot pass more than 3 options to the 'yesno'-filter (got: '%s').", paramString),
}
}
if len(customChoices) < 2 {
return nil, &Error{
Sender: "filter:yesno",
OrigError: fmt.Errorf("You must pass either no or at least 2 arguments to the 'yesno'-filter (got: '%s').", paramString),
}
}
// Map to the options now
choices[0] = customChoices[0]
choices[1] = customChoices[1]
if len(customChoices) == 3 {
choices[2] = customChoices[2]
}
}
// maybe
if in.IsNil() {
return AsValue(choices[2]), nil
}
// yes
if in.IsTrue() {
return AsValue(choices[0]), nil
}
// no
return AsValue(choices[1]), nil
}

View file

@ -1,15 +0,0 @@
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
}

View file

@ -1,432 +0,0 @@
package pongo2
import (
"fmt"
"strings"
"unicode/utf8"
"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:])
}
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
}

View file

@ -1,16 +0,0 @@
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
}

View file

@ -1,23 +0,0 @@
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
}

View file

@ -1,16 +0,0 @@
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
}

View file

@ -1,26 +0,0 @@
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
}

View file

@ -1,309 +0,0 @@
package pongo2
import (
"fmt"
"strings"
"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)
}

View file

@ -1,59 +0,0 @@
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
}

View file

@ -1,517 +0,0 @@
package pongo2
import (
"fmt"
"math"
)
type Expression struct {
// TODO: Add location token?
expr1 IEvaluator
expr2 IEvaluator
opToken *Token
}
type relationalExpression struct {
// TODO: Add location token?
expr1 IEvaluator
expr2 IEvaluator
opToken *Token
}
type simpleExpression struct {
negate bool
negativeSign bool
term1 IEvaluator
term2 IEvaluator
opToken *Token
}
type term struct {
// TODO: Add location token?
factor1 IEvaluator
factor2 IEvaluator
opToken *Token
}
type power struct {
// TODO: Add location token?
power1 IEvaluator
power2 IEvaluator
}
func (expr *Expression) FilterApplied(name string) bool {
return expr.expr1.FilterApplied(name) && (expr.expr2 == nil ||
(expr.expr2 != nil && expr.expr2.FilterApplied(name)))
}
func (expr *relationalExpression) FilterApplied(name string) bool {
return expr.expr1.FilterApplied(name) && (expr.expr2 == nil ||
(expr.expr2 != nil && expr.expr2.FilterApplied(name)))
}
func (expr *simpleExpression) FilterApplied(name string) bool {
return expr.term1.FilterApplied(name) && (expr.term2 == nil ||
(expr.term2 != nil && expr.term2.FilterApplied(name)))
}
func (expr *term) FilterApplied(name string) bool {
return expr.factor1.FilterApplied(name) && (expr.factor2 == nil ||
(expr.factor2 != nil && expr.factor2.FilterApplied(name)))
}
func (expr *power) FilterApplied(name string) bool {
return expr.power1.FilterApplied(name) && (expr.power2 == nil ||
(expr.power2 != nil && expr.power2.FilterApplied(name)))
}
func (expr *Expression) GetPositionToken() *Token {
return expr.expr1.GetPositionToken()
}
func (expr *relationalExpression) GetPositionToken() *Token {
return expr.expr1.GetPositionToken()
}
func (expr *simpleExpression) GetPositionToken() *Token {
return expr.term1.GetPositionToken()
}
func (expr *term) GetPositionToken() *Token {
return expr.factor1.GetPositionToken()
}
func (expr *power) GetPositionToken() *Token {
return expr.power1.GetPositionToken()
}
func (expr *Expression) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
value, err := expr.Evaluate(ctx)
if err != nil {
return err
}
writer.WriteString(value.String())
return nil
}
func (expr *relationalExpression) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
value, err := expr.Evaluate(ctx)
if err != nil {
return err
}
writer.WriteString(value.String())
return nil
}
func (expr *simpleExpression) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
value, err := expr.Evaluate(ctx)
if err != nil {
return err
}
writer.WriteString(value.String())
return nil
}
func (expr *term) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
value, err := expr.Evaluate(ctx)
if err != nil {
return err
}
writer.WriteString(value.String())
return nil
}
func (expr *power) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
value, err := expr.Evaluate(ctx)
if err != nil {
return err
}
writer.WriteString(value.String())
return nil
}
func (expr *Expression) Evaluate(ctx *ExecutionContext) (*Value, *Error) {
v1, err := expr.expr1.Evaluate(ctx)
if err != nil {
return nil, err
}
if expr.expr2 != nil {
switch expr.opToken.Val {
case "and", "&&":
if !v1.IsTrue() {
return AsValue(false), nil
} else {
v2, err := expr.expr2.Evaluate(ctx)
if err != nil {
return nil, err
}
return AsValue(v2.IsTrue()), nil
}
case "or", "||":
if v1.IsTrue() {
return AsValue(true), nil
} else {
v2, err := expr.expr2.Evaluate(ctx)
if err != nil {
return nil, err
}
return AsValue(v2.IsTrue()), nil
}
default:
return nil, ctx.Error(fmt.Sprintf("unimplemented: %s", expr.opToken.Val), expr.opToken)
}
} else {
return v1, nil
}
}
func (expr *relationalExpression) Evaluate(ctx *ExecutionContext) (*Value, *Error) {
v1, err := expr.expr1.Evaluate(ctx)
if err != nil {
return nil, err
}
if expr.expr2 != nil {
v2, err := expr.expr2.Evaluate(ctx)
if err != nil {
return nil, err
}
switch expr.opToken.Val {
case "<=":
if v1.IsFloat() || v2.IsFloat() {
return AsValue(v1.Float() <= v2.Float()), nil
}
if v1.IsTime() && v2.IsTime() {
tm1, tm2 := v1.Time(), v2.Time()
return AsValue(tm1.Before(tm2) || tm1.Equal(tm2)), nil
}
return AsValue(v1.Integer() <= v2.Integer()), nil
case ">=":
if v1.IsFloat() || v2.IsFloat() {
return AsValue(v1.Float() >= v2.Float()), nil
}
if v1.IsTime() && v2.IsTime() {
tm1, tm2 := v1.Time(), v2.Time()
return AsValue(tm1.After(tm2) || tm1.Equal(tm2)), nil
}
return AsValue(v1.Integer() >= v2.Integer()), nil
case "==":
return AsValue(v1.EqualValueTo(v2)), nil
case ">":
if v1.IsFloat() || v2.IsFloat() {
return AsValue(v1.Float() > v2.Float()), nil
}
if v1.IsTime() && v2.IsTime() {
return AsValue(v1.Time().After(v2.Time())), nil
}
return AsValue(v1.Integer() > v2.Integer()), nil
case "<":
if v1.IsFloat() || v2.IsFloat() {
return AsValue(v1.Float() < v2.Float()), nil
}
if v1.IsTime() && v2.IsTime() {
return AsValue(v1.Time().Before(v2.Time())), nil
}
return AsValue(v1.Integer() < v2.Integer()), nil
case "!=", "<>":
return AsValue(!v1.EqualValueTo(v2)), nil
case "in":
return AsValue(v2.Contains(v1)), nil
default:
return nil, ctx.Error(fmt.Sprintf("unimplemented: %s", expr.opToken.Val), expr.opToken)
}
} else {
return v1, nil
}
}
func (expr *simpleExpression) Evaluate(ctx *ExecutionContext) (*Value, *Error) {
t1, err := expr.term1.Evaluate(ctx)
if err != nil {
return nil, err
}
result := t1
if expr.negate {
result = result.Negate()
}
if expr.negativeSign {
if result.IsNumber() {
switch {
case result.IsFloat():
result = AsValue(-1 * result.Float())
case result.IsInteger():
result = AsValue(-1 * result.Integer())
default:
return nil, ctx.Error("Operation between a number and a non-(float/integer) is not possible", nil)
}
} else {
return nil, ctx.Error("Negative sign on a non-number expression", expr.GetPositionToken())
}
}
if expr.term2 != nil {
t2, err := expr.term2.Evaluate(ctx)
if err != nil {
return nil, err
}
switch expr.opToken.Val {
case "+":
if result.IsFloat() || t2.IsFloat() {
// Result will be a float
return AsValue(result.Float() + t2.Float()), nil
}
// Result will be an integer
return AsValue(result.Integer() + t2.Integer()), nil
case "-":
if result.IsFloat() || t2.IsFloat() {
// Result will be a float
return AsValue(result.Float() - t2.Float()), nil
}
// Result will be an integer
return AsValue(result.Integer() - t2.Integer()), nil
default:
return nil, ctx.Error("Unimplemented", expr.GetPositionToken())
}
}
return result, nil
}
func (expr *term) Evaluate(ctx *ExecutionContext) (*Value, *Error) {
f1, err := expr.factor1.Evaluate(ctx)
if err != nil {
return nil, err
}
if expr.factor2 != nil {
f2, err := expr.factor2.Evaluate(ctx)
if err != nil {
return nil, err
}
switch expr.opToken.Val {
case "*":
if f1.IsFloat() || f2.IsFloat() {
// Result will be float
return AsValue(f1.Float() * f2.Float()), nil
}
// Result will be int
return AsValue(f1.Integer() * f2.Integer()), nil
case "/":
if f1.IsFloat() || f2.IsFloat() {
// Result will be float
return AsValue(f1.Float() / f2.Float()), nil
}
// Result will be int
return AsValue(f1.Integer() / f2.Integer()), nil
case "%":
// Result will be int
return AsValue(f1.Integer() % f2.Integer()), nil
default:
return nil, ctx.Error("unimplemented", expr.opToken)
}
} else {
return f1, nil
}
}
func (expr *power) Evaluate(ctx *ExecutionContext) (*Value, *Error) {
p1, err := expr.power1.Evaluate(ctx)
if err != nil {
return nil, err
}
if expr.power2 != nil {
p2, err := expr.power2.Evaluate(ctx)
if err != nil {
return nil, err
}
return AsValue(math.Pow(p1.Float(), p2.Float())), nil
}
return p1, nil
}
func (p *Parser) parseFactor() (IEvaluator, *Error) {
if p.Match(TokenSymbol, "(") != nil {
expr, err := p.ParseExpression()
if err != nil {
return nil, err
}
if p.Match(TokenSymbol, ")") == nil {
return nil, p.Error("Closing bracket expected after expression", nil)
}
return expr, nil
}
return p.parseVariableOrLiteralWithFilter()
}
func (p *Parser) parsePower() (IEvaluator, *Error) {
pw := new(power)
power1, err := p.parseFactor()
if err != nil {
return nil, err
}
pw.power1 = power1
if p.Match(TokenSymbol, "^") != nil {
power2, err := p.parsePower()
if err != nil {
return nil, err
}
pw.power2 = power2
}
if pw.power2 == nil {
// Shortcut for faster evaluation
return pw.power1, nil
}
return pw, nil
}
func (p *Parser) parseTerm() (IEvaluator, *Error) {
returnTerm := new(term)
factor1, err := p.parsePower()
if err != nil {
return nil, err
}
returnTerm.factor1 = factor1
for p.PeekOne(TokenSymbol, "*", "/", "%") != nil {
if returnTerm.opToken != nil {
// Create new sub-term
returnTerm = &term{
factor1: returnTerm,
}
}
op := p.Current()
p.Consume()
factor2, err := p.parsePower()
if err != nil {
return nil, err
}
returnTerm.opToken = op
returnTerm.factor2 = factor2
}
if returnTerm.opToken == nil {
// Shortcut for faster evaluation
return returnTerm.factor1, nil
}
return returnTerm, nil
}
func (p *Parser) parseSimpleExpression() (IEvaluator, *Error) {
expr := new(simpleExpression)
if sign := p.MatchOne(TokenSymbol, "+", "-"); sign != nil {
if sign.Val == "-" {
expr.negativeSign = true
}
}
if p.Match(TokenSymbol, "!") != nil || p.Match(TokenKeyword, "not") != nil {
expr.negate = true
}
term1, err := p.parseTerm()
if err != nil {
return nil, err
}
expr.term1 = term1
for p.PeekOne(TokenSymbol, "+", "-") != nil {
if expr.opToken != nil {
// New sub expr
expr = &simpleExpression{
term1: expr,
}
}
op := p.Current()
p.Consume()
term2, err := p.parseTerm()
if err != nil {
return nil, err
}
expr.term2 = term2
expr.opToken = op
}
if expr.negate == false && expr.negativeSign == false && expr.term2 == nil {
// Shortcut for faster evaluation
return expr.term1, nil
}
return expr, nil
}
func (p *Parser) parseRelationalExpression() (IEvaluator, *Error) {
expr1, err := p.parseSimpleExpression()
if err != nil {
return nil, err
}
expr := &relationalExpression{
expr1: expr1,
}
if t := p.MatchOne(TokenSymbol, "==", "<=", ">=", "!=", "<>", ">", "<"); t != nil {
expr2, err := p.parseRelationalExpression()
if err != nil {
return nil, err
}
expr.opToken = t
expr.expr2 = expr2
} else if t := p.MatchOne(TokenKeyword, "in"); t != nil {
expr2, err := p.parseSimpleExpression()
if err != nil {
return nil, err
}
expr.opToken = t
expr.expr2 = expr2
}
if expr.expr2 == nil {
// Shortcut for faster evaluation
return expr.expr1, nil
}
return expr, nil
}
func (p *Parser) ParseExpression() (IEvaluator, *Error) {
rexpr1, err := p.parseRelationalExpression()
if err != nil {
return nil, err
}
exp := &Expression{
expr1: rexpr1,
}
if p.PeekOne(TokenSymbol, "&&", "||") != nil || p.PeekOne(TokenKeyword, "and", "or") != nil {
op := p.Current()
p.Consume()
expr2, err := p.ParseExpression()
if err != nil {
return nil, err
}
exp.expr2 = expr2
exp.opToken = op
}
if exp.expr2 == nil {
// Shortcut for faster evaluation
return exp.expr1, nil
}
return exp, nil
}

View file

@ -1,14 +0,0 @@
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
}

View file

@ -1,133 +0,0 @@
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"
)
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 fmt.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 fmt.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)
}

View file

@ -1,52 +0,0 @@
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)
}

View file

@ -1,129 +0,0 @@
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)
}

View file

@ -1,27 +0,0 @@
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)
}

View file

@ -1,106 +0,0 @@
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)
}

View file

@ -1,52 +0,0 @@
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)
}

View file

@ -1,95 +0,0 @@
package pongo2
import (
"bytes"
)
type nodeFilterCall struct {
name string
paramExpr IEvaluator
}
type tagFilterNode struct {
position *Token
bodyWrapper *NodeWrapper
filterChain []*nodeFilterCall
}
func (node *tagFilterNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
temp := bytes.NewBuffer(make([]byte, 0, 1024)) // 1 KiB size
err := node.bodyWrapper.Execute(ctx, temp)
if err != nil {
return err
}
value := AsValue(temp.String())
for _, call := range node.filterChain {
var param *Value
if call.paramExpr != nil {
param, err = call.paramExpr.Evaluate(ctx)
if err != nil {
return err
}
} else {
param = AsValue(nil)
}
value, err = ApplyFilter(call.name, value, param)
if err != nil {
return ctx.Error(err.Error(), node.position)
}
}
writer.WriteString(value.String())
return nil
}
func tagFilterParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
filterNode := &tagFilterNode{
position: start,
}
wrapper, _, err := doc.WrapUntilTag("endfilter")
if err != nil {
return nil, err
}
filterNode.bodyWrapper = wrapper
for arguments.Remaining() > 0 {
filterCall := &nodeFilterCall{}
nameToken := arguments.MatchType(TokenIdentifier)
if nameToken == nil {
return nil, arguments.Error("Expected a filter name (identifier).", nil)
}
filterCall.name = nameToken.Val
if arguments.MatchOne(TokenSymbol, ":") != nil {
// Filter parameter
// NOTICE: we can't use ParseExpression() here, because it would parse the next filter "|..." as well in the argument list
expr, err := arguments.parseVariableOrLiteral()
if err != nil {
return nil, err
}
filterCall.paramExpr = expr
}
filterNode.filterChain = append(filterNode.filterChain, filterCall)
if arguments.MatchOne(TokenSymbol, "|") == nil {
break
}
}
if arguments.Remaining() > 0 {
return nil, arguments.Error("Malformed filter-tag arguments.", nil)
}
return filterNode, nil
}
func init() {
RegisterTag("filter", tagFilterParser)
}

View file

@ -1,49 +0,0 @@
package pongo2
type tagFirstofNode struct {
position *Token
args []IEvaluator
}
func (node *tagFirstofNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
for _, arg := range node.args {
val, err := arg.Evaluate(ctx)
if err != nil {
return err
}
if val.IsTrue() {
if ctx.Autoescape && !arg.FilterApplied("safe") {
val, err = ApplyFilter("escape", val, nil)
if err != nil {
return err
}
}
writer.WriteString(val.String())
return nil
}
}
return nil
}
func tagFirstofParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
firstofNode := &tagFirstofNode{
position: start,
}
for arguments.Remaining() > 0 {
node, err := arguments.ParseExpression()
if err != nil {
return nil, err
}
firstofNode.args = append(firstofNode.args, node)
}
return firstofNode, nil
}
func init() {
RegisterTag("firstof", tagFirstofParser)
}

View file

@ -1,159 +0,0 @@
package pongo2
type tagForNode struct {
key string
value string // only for maps: for key, value in map
objectEvaluator IEvaluator
reversed bool
sorted bool
bodyWrapper *NodeWrapper
emptyWrapper *NodeWrapper
}
type tagForLoopInformation struct {
Counter int
Counter0 int
Revcounter int
Revcounter0 int
First bool
Last bool
Parentloop *tagForLoopInformation
}
func (node *tagForNode) Execute(ctx *ExecutionContext, writer TemplateWriter) (forError *Error) {
// Backup forloop (as parentloop in public context), key-name and value-name
forCtx := NewChildExecutionContext(ctx)
parentloop := forCtx.Private["forloop"]
// Create loop struct
loopInfo := &tagForLoopInformation{
First: true,
}
// Is it a loop in a loop?
if parentloop != nil {
loopInfo.Parentloop = parentloop.(*tagForLoopInformation)
}
// Register loopInfo in public context
forCtx.Private["forloop"] = loopInfo
obj, err := node.objectEvaluator.Evaluate(forCtx)
if err != nil {
return err
}
obj.IterateOrder(func(idx, count int, key, value *Value) bool {
// There's something to iterate over (correct type and at least 1 item)
// Update loop infos and public context
forCtx.Private[node.key] = key
if value != nil {
forCtx.Private[node.value] = value
}
loopInfo.Counter = idx + 1
loopInfo.Counter0 = idx
if idx == 1 {
loopInfo.First = false
}
if idx+1 == count {
loopInfo.Last = true
}
loopInfo.Revcounter = count - idx // TODO: Not sure about this, have to look it up
loopInfo.Revcounter0 = count - (idx + 1) // TODO: Not sure about this, have to look it up
// Render elements with updated context
err := node.bodyWrapper.Execute(forCtx, writer)
if err != nil {
forError = err
return false
}
return true
}, func() {
// Nothing to iterate over (maybe wrong type or no items)
if node.emptyWrapper != nil {
err := node.emptyWrapper.Execute(forCtx, writer)
if err != nil {
forError = err
}
}
}, node.reversed, node.sorted)
return forError
}
func tagForParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
forNode := &tagForNode{}
// Arguments parsing
var valueToken *Token
keyToken := arguments.MatchType(TokenIdentifier)
if keyToken == nil {
return nil, arguments.Error("Expected an key identifier as first argument for 'for'-tag", nil)
}
if arguments.Match(TokenSymbol, ",") != nil {
// Value name is provided
valueToken = arguments.MatchType(TokenIdentifier)
if valueToken == nil {
return nil, arguments.Error("Value name must be an identifier.", nil)
}
}
if arguments.Match(TokenKeyword, "in") == nil {
return nil, arguments.Error("Expected keyword 'in'.", nil)
}
objectEvaluator, err := arguments.ParseExpression()
if err != nil {
return nil, err
}
forNode.objectEvaluator = objectEvaluator
forNode.key = keyToken.Val
if valueToken != nil {
forNode.value = valueToken.Val
}
if arguments.MatchOne(TokenIdentifier, "reversed") != nil {
forNode.reversed = true
}
if arguments.MatchOne(TokenIdentifier, "sorted") != nil {
forNode.sorted = true
}
if arguments.Remaining() > 0 {
return nil, arguments.Error("Malformed for-loop arguments.", nil)
}
// Body wrapping
wrapper, endargs, err := doc.WrapUntilTag("empty", "endfor")
if err != nil {
return nil, err
}
forNode.bodyWrapper = wrapper
if endargs.Count() > 0 {
return nil, endargs.Error("Arguments not allowed here.", nil)
}
if wrapper.Endtag == "empty" {
// if there's an else in the if-statement, we need the else-Block as well
wrapper, endargs, err = doc.WrapUntilTag("endfor")
if err != nil {
return nil, err
}
forNode.emptyWrapper = wrapper
if endargs.Count() > 0 {
return nil, endargs.Error("Arguments not allowed here.", nil)
}
}
return forNode, nil
}
func init() {
RegisterTag("for", tagForParser)
}

View file

@ -1,76 +0,0 @@
package pongo2
type tagIfNode struct {
conditions []IEvaluator
wrappers []*NodeWrapper
}
func (node *tagIfNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
for i, condition := range node.conditions {
result, err := condition.Evaluate(ctx)
if err != nil {
return err
}
if result.IsTrue() {
return node.wrappers[i].Execute(ctx, writer)
}
// Last condition?
if len(node.conditions) == i+1 && len(node.wrappers) > i+1 {
return node.wrappers[i+1].Execute(ctx, writer)
}
}
return nil
}
func tagIfParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
ifNode := &tagIfNode{}
// Parse first and main IF condition
condition, err := arguments.ParseExpression()
if err != nil {
return nil, err
}
ifNode.conditions = append(ifNode.conditions, condition)
if arguments.Remaining() > 0 {
return nil, arguments.Error("If-condition is malformed.", nil)
}
// Check the rest
for {
wrapper, tagArgs, err := doc.WrapUntilTag("elif", "else", "endif")
if err != nil {
return nil, err
}
ifNode.wrappers = append(ifNode.wrappers, wrapper)
if wrapper.Endtag == "elif" {
// elif can take a condition
condition, err = tagArgs.ParseExpression()
if err != nil {
return nil, err
}
ifNode.conditions = append(ifNode.conditions, condition)
if tagArgs.Remaining() > 0 {
return nil, tagArgs.Error("Elif-condition is malformed.", nil)
}
} else {
if tagArgs.Count() > 0 {
// else/endif can't take any conditions
return nil, tagArgs.Error("Arguments not allowed here.", nil)
}
}
if wrapper.Endtag == "endif" {
break
}
}
return ifNode, nil
}
func init() {
RegisterTag("if", tagIfParser)
}

View file

@ -1,116 +0,0 @@
package pongo2
import (
"bytes"
)
type tagIfchangedNode struct {
watchedExpr []IEvaluator
lastValues []*Value
lastContent []byte
thenWrapper *NodeWrapper
elseWrapper *NodeWrapper
}
func (node *tagIfchangedNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
if len(node.watchedExpr) == 0 {
// Check against own rendered body
buf := bytes.NewBuffer(make([]byte, 0, 1024)) // 1 KiB
err := node.thenWrapper.Execute(ctx, buf)
if err != nil {
return err
}
bufBytes := buf.Bytes()
if !bytes.Equal(node.lastContent, bufBytes) {
// Rendered content changed, output it
writer.Write(bufBytes)
node.lastContent = bufBytes
}
} else {
nowValues := make([]*Value, 0, len(node.watchedExpr))
for _, expr := range node.watchedExpr {
val, err := expr.Evaluate(ctx)
if err != nil {
return err
}
nowValues = append(nowValues, val)
}
// Compare old to new values now
changed := len(node.lastValues) == 0
for idx, oldVal := range node.lastValues {
if !oldVal.EqualValueTo(nowValues[idx]) {
changed = true
break // we can stop here because ONE value changed
}
}
node.lastValues = nowValues
if changed {
// Render thenWrapper
err := node.thenWrapper.Execute(ctx, writer)
if err != nil {
return err
}
} else {
// Render elseWrapper
err := node.elseWrapper.Execute(ctx, writer)
if err != nil {
return err
}
}
}
return nil
}
func tagIfchangedParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
ifchangedNode := &tagIfchangedNode{}
for arguments.Remaining() > 0 {
// Parse condition
expr, err := arguments.ParseExpression()
if err != nil {
return nil, err
}
ifchangedNode.watchedExpr = append(ifchangedNode.watchedExpr, expr)
}
if arguments.Remaining() > 0 {
return nil, arguments.Error("Ifchanged-arguments are malformed.", nil)
}
// Wrap then/else-blocks
wrapper, endargs, err := doc.WrapUntilTag("else", "endifchanged")
if err != nil {
return nil, err
}
ifchangedNode.thenWrapper = wrapper
if endargs.Count() > 0 {
return nil, endargs.Error("Arguments not allowed here.", nil)
}
if wrapper.Endtag == "else" {
// if there's an else in the if-statement, we need the else-Block as well
wrapper, endargs, err = doc.WrapUntilTag("endifchanged")
if err != nil {
return nil, err
}
ifchangedNode.elseWrapper = wrapper
if endargs.Count() > 0 {
return nil, endargs.Error("Arguments not allowed here.", nil)
}
}
return ifchangedNode, nil
}
func init() {
RegisterTag("ifchanged", tagIfchangedParser)
}

View file

@ -1,78 +0,0 @@
package pongo2
type tagIfEqualNode struct {
var1, var2 IEvaluator
thenWrapper *NodeWrapper
elseWrapper *NodeWrapper
}
func (node *tagIfEqualNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
r1, err := node.var1.Evaluate(ctx)
if err != nil {
return err
}
r2, err := node.var2.Evaluate(ctx)
if err != nil {
return err
}
result := r1.EqualValueTo(r2)
if result {
return node.thenWrapper.Execute(ctx, writer)
}
if node.elseWrapper != nil {
return node.elseWrapper.Execute(ctx, writer)
}
return nil
}
func tagIfEqualParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
ifequalNode := &tagIfEqualNode{}
// Parse two expressions
var1, err := arguments.ParseExpression()
if err != nil {
return nil, err
}
var2, err := arguments.ParseExpression()
if err != nil {
return nil, err
}
ifequalNode.var1 = var1
ifequalNode.var2 = var2
if arguments.Remaining() > 0 {
return nil, arguments.Error("ifequal only takes 2 arguments.", nil)
}
// Wrap then/else-blocks
wrapper, endargs, err := doc.WrapUntilTag("else", "endifequal")
if err != nil {
return nil, err
}
ifequalNode.thenWrapper = wrapper
if endargs.Count() > 0 {
return nil, endargs.Error("Arguments not allowed here.", nil)
}
if wrapper.Endtag == "else" {
// if there's an else in the if-statement, we need the else-Block as well
wrapper, endargs, err = doc.WrapUntilTag("endifequal")
if err != nil {
return nil, err
}
ifequalNode.elseWrapper = wrapper
if endargs.Count() > 0 {
return nil, endargs.Error("Arguments not allowed here.", nil)
}
}
return ifequalNode, nil
}
func init() {
RegisterTag("ifequal", tagIfEqualParser)
}

View file

@ -1,78 +0,0 @@
package pongo2
type tagIfNotEqualNode struct {
var1, var2 IEvaluator
thenWrapper *NodeWrapper
elseWrapper *NodeWrapper
}
func (node *tagIfNotEqualNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
r1, err := node.var1.Evaluate(ctx)
if err != nil {
return err
}
r2, err := node.var2.Evaluate(ctx)
if err != nil {
return err
}
result := !r1.EqualValueTo(r2)
if result {
return node.thenWrapper.Execute(ctx, writer)
}
if node.elseWrapper != nil {
return node.elseWrapper.Execute(ctx, writer)
}
return nil
}
func tagIfNotEqualParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
ifnotequalNode := &tagIfNotEqualNode{}
// Parse two expressions
var1, err := arguments.ParseExpression()
if err != nil {
return nil, err
}
var2, err := arguments.ParseExpression()
if err != nil {
return nil, err
}
ifnotequalNode.var1 = var1
ifnotequalNode.var2 = var2
if arguments.Remaining() > 0 {
return nil, arguments.Error("ifequal only takes 2 arguments.", nil)
}
// Wrap then/else-blocks
wrapper, endargs, err := doc.WrapUntilTag("else", "endifnotequal")
if err != nil {
return nil, err
}
ifnotequalNode.thenWrapper = wrapper
if endargs.Count() > 0 {
return nil, endargs.Error("Arguments not allowed here.", nil)
}
if wrapper.Endtag == "else" {
// if there's an else in the if-statement, we need the else-Block as well
wrapper, endargs, err = doc.WrapUntilTag("endifnotequal")
if err != nil {
return nil, err
}
ifnotequalNode.elseWrapper = wrapper
if endargs.Count() > 0 {
return nil, endargs.Error("Arguments not allowed here.", nil)
}
}
return ifnotequalNode, nil
}
func init() {
RegisterTag("ifnotequal", tagIfNotEqualParser)
}

View file

@ -1,84 +0,0 @@
package pongo2
import (
"fmt"
)
type tagImportNode struct {
position *Token
filename string
macros map[string]*tagMacroNode // alias/name -> macro instance
}
func (node *tagImportNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
for name, macro := range node.macros {
func(name string, macro *tagMacroNode) {
ctx.Private[name] = func(args ...*Value) *Value {
return macro.call(ctx, args...)
}
}(name, macro)
}
return nil
}
func tagImportParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
importNode := &tagImportNode{
position: start,
macros: make(map[string]*tagMacroNode),
}
filenameToken := arguments.MatchType(TokenString)
if filenameToken == nil {
return nil, arguments.Error("Import-tag needs a filename as string.", nil)
}
importNode.filename = doc.template.set.resolveFilename(doc.template, filenameToken.Val)
if arguments.Remaining() == 0 {
return nil, arguments.Error("You must at least specify one macro to import.", nil)
}
// Compile the given template
tpl, err := doc.template.set.FromFile(importNode.filename)
if err != nil {
return nil, err.(*Error).updateFromTokenIfNeeded(doc.template, start)
}
for arguments.Remaining() > 0 {
macroNameToken := arguments.MatchType(TokenIdentifier)
if macroNameToken == nil {
return nil, arguments.Error("Expected macro name (identifier).", nil)
}
asName := macroNameToken.Val
if arguments.Match(TokenKeyword, "as") != nil {
aliasToken := arguments.MatchType(TokenIdentifier)
if aliasToken == nil {
return nil, arguments.Error("Expected macro alias name (identifier).", nil)
}
asName = aliasToken.Val
}
macroInstance, has := tpl.exportedMacros[macroNameToken.Val]
if !has {
return nil, arguments.Error(fmt.Sprintf("Macro '%s' not found (or not exported) in '%s'.", macroNameToken.Val,
importNode.filename), macroNameToken)
}
importNode.macros[asName] = macroInstance
if arguments.Remaining() == 0 {
break
}
if arguments.Match(TokenSymbol, ",") == nil {
return nil, arguments.Error("Expected ','.", nil)
}
}
return importNode, nil
}
func init() {
RegisterTag("import", tagImportParser)
}

View file

@ -1,146 +0,0 @@
package pongo2
type tagIncludeNode struct {
tpl *Template
filenameEvaluator IEvaluator
lazy bool
only bool
filename string
withPairs map[string]IEvaluator
ifExists bool
}
func (node *tagIncludeNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
// Building the context for the template
includeCtx := make(Context)
// Fill the context with all data from the parent
if !node.only {
includeCtx.Update(ctx.Public)
includeCtx.Update(ctx.Private)
}
// Put all custom with-pairs into the context
for key, value := range node.withPairs {
val, err := value.Evaluate(ctx)
if err != nil {
return err
}
includeCtx[key] = val
}
// Execute the template
if node.lazy {
// Evaluate the filename
filename, err := node.filenameEvaluator.Evaluate(ctx)
if err != nil {
return err
}
if filename.String() == "" {
return ctx.Error("Filename for 'include'-tag evaluated to an empty string.", nil)
}
// Get include-filename
includedFilename := ctx.template.set.resolveFilename(ctx.template, filename.String())
includedTpl, err2 := ctx.template.set.FromFile(includedFilename)
if err2 != nil {
// if this is ReadFile error, and "if_exists" flag is enabled
if node.ifExists && err2.(*Error).Sender == "fromfile" {
return nil
}
return err2.(*Error)
}
err2 = includedTpl.ExecuteWriter(includeCtx, writer)
if err2 != nil {
return err2.(*Error)
}
return nil
}
// Template is already parsed with static filename
err := node.tpl.ExecuteWriter(includeCtx, writer)
if err != nil {
return err.(*Error)
}
return nil
}
type tagIncludeEmptyNode struct{}
func (node *tagIncludeEmptyNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
return nil
}
func tagIncludeParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
includeNode := &tagIncludeNode{
withPairs: make(map[string]IEvaluator),
}
if filenameToken := arguments.MatchType(TokenString); filenameToken != nil {
// prepared, static template
// "if_exists" flag
ifExists := arguments.Match(TokenIdentifier, "if_exists") != nil
// Get include-filename
includedFilename := doc.template.set.resolveFilename(doc.template, filenameToken.Val)
// Parse the parent
includeNode.filename = includedFilename
includedTpl, err := doc.template.set.FromFile(includedFilename)
if err != nil {
// if this is ReadFile error, and "if_exists" token presents we should create and empty node
if err.(*Error).Sender == "fromfile" && ifExists {
return &tagIncludeEmptyNode{}, nil
}
return nil, err.(*Error).updateFromTokenIfNeeded(doc.template, filenameToken)
}
includeNode.tpl = includedTpl
} else {
// No String, then the user wants to use lazy-evaluation (slower, but possible)
filenameEvaluator, err := arguments.ParseExpression()
if err != nil {
return nil, err.updateFromTokenIfNeeded(doc.template, filenameToken)
}
includeNode.filenameEvaluator = filenameEvaluator
includeNode.lazy = true
includeNode.ifExists = arguments.Match(TokenIdentifier, "if_exists") != nil // "if_exists" flag
}
// After having parsed the filename we're gonna parse the with+only options
if arguments.Match(TokenIdentifier, "with") != nil {
for arguments.Remaining() > 0 {
// We have at least one key=expr pair (because of starting "with")
keyToken := arguments.MatchType(TokenIdentifier)
if keyToken == nil {
return nil, arguments.Error("Expected an identifier", nil)
}
if arguments.Match(TokenSymbol, "=") == nil {
return nil, arguments.Error("Expected '='.", nil)
}
valueExpr, err := arguments.ParseExpression()
if err != nil {
return nil, err.updateFromTokenIfNeeded(doc.template, keyToken)
}
includeNode.withPairs[keyToken.Val] = valueExpr
// Only?
if arguments.Match(TokenIdentifier, "only") != nil {
includeNode.only = true
break // stop parsing arguments because it's the last option
}
}
}
if arguments.Remaining() > 0 {
return nil, arguments.Error("Malformed 'include'-tag arguments.", nil)
}
return includeNode, nil
}
func init() {
RegisterTag("include", tagIncludeParser)
}

View file

@ -1,132 +0,0 @@
package pongo2
import (
"fmt"
"math/rand"
"strings"
"time"
)
var (
tagLoremParagraphs = strings.Split(tagLoremText, "\n")
tagLoremWords = strings.Fields(tagLoremText)
)
type tagLoremNode struct {
position *Token
count int // number of paragraphs
method string // w = words, p = HTML paragraphs, b = plain-text (default is b)
random bool // does not use the default paragraph "Lorem ipsum dolor sit amet, ..."
}
func (node *tagLoremNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
switch node.method {
case "b":
if node.random {
for i := 0; i < node.count; i++ {
if i > 0 {
writer.WriteString("\n")
}
par := tagLoremParagraphs[rand.Intn(len(tagLoremParagraphs))]
writer.WriteString(par)
}
} else {
for i := 0; i < node.count; i++ {
if i > 0 {
writer.WriteString("\n")
}
par := tagLoremParagraphs[i%len(tagLoremParagraphs)]
writer.WriteString(par)
}
}
case "w":
if node.random {
for i := 0; i < node.count; i++ {
if i > 0 {
writer.WriteString(" ")
}
word := tagLoremWords[rand.Intn(len(tagLoremWords))]
writer.WriteString(word)
}
} else {
for i := 0; i < node.count; i++ {
if i > 0 {
writer.WriteString(" ")
}
word := tagLoremWords[i%len(tagLoremWords)]
writer.WriteString(word)
}
}
case "p":
if node.random {
for i := 0; i < node.count; i++ {
if i > 0 {
writer.WriteString("\n")
}
writer.WriteString("<p>")
par := tagLoremParagraphs[rand.Intn(len(tagLoremParagraphs))]
writer.WriteString(par)
writer.WriteString("</p>")
}
} else {
for i := 0; i < node.count; i++ {
if i > 0 {
writer.WriteString("\n")
}
writer.WriteString("<p>")
par := tagLoremParagraphs[i%len(tagLoremParagraphs)]
writer.WriteString(par)
writer.WriteString("</p>")
}
}
default:
return ctx.OrigError(fmt.Errorf("unsupported method: %s", node.method), nil)
}
return nil
}
func tagLoremParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
loremNode := &tagLoremNode{
position: start,
count: 1,
method: "b",
}
if countToken := arguments.MatchType(TokenNumber); countToken != nil {
loremNode.count = AsValue(countToken.Val).Integer()
}
if methodToken := arguments.MatchType(TokenIdentifier); methodToken != nil {
if methodToken.Val != "w" && methodToken.Val != "p" && methodToken.Val != "b" {
return nil, arguments.Error("lorem-method must be either 'w', 'p' or 'b'.", nil)
}
loremNode.method = methodToken.Val
}
if arguments.MatchOne(TokenIdentifier, "random") != nil {
loremNode.random = true
}
if arguments.Remaining() > 0 {
return nil, arguments.Error("Malformed lorem-tag arguments.", nil)
}
return loremNode, nil
}
func init() {
rand.Seed(time.Now().Unix())
RegisterTag("lorem", tagLoremParser)
}
const tagLoremText = `Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.
Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.
At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.
Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.`

View file

@ -1,149 +0,0 @@
package pongo2
import (
"bytes"
"fmt"
)
type tagMacroNode struct {
position *Token
name string
argsOrder []string
args map[string]IEvaluator
exported bool
wrapper *NodeWrapper
}
func (node *tagMacroNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
ctx.Private[node.name] = func(args ...*Value) *Value {
return node.call(ctx, args...)
}
return nil
}
func (node *tagMacroNode) call(ctx *ExecutionContext, args ...*Value) *Value {
argsCtx := make(Context)
for k, v := range node.args {
if v == nil {
// User did not provided a default value
argsCtx[k] = nil
} else {
// Evaluate the default value
valueExpr, err := v.Evaluate(ctx)
if err != nil {
ctx.Logf(err.Error())
return AsSafeValue(err.Error())
}
argsCtx[k] = valueExpr
}
}
if len(args) > len(node.argsOrder) {
// Too many arguments, we're ignoring them and just logging into debug mode.
err := ctx.Error(fmt.Sprintf("Macro '%s' called with too many arguments (%d instead of %d).",
node.name, len(args), len(node.argsOrder)), nil).updateFromTokenIfNeeded(ctx.template, node.position)
ctx.Logf(err.Error()) // TODO: This is a workaround, because the error is not returned yet to the Execution()-methods
return AsSafeValue(err.Error())
}
// Make a context for the macro execution
macroCtx := NewChildExecutionContext(ctx)
// Register all arguments in the private context
macroCtx.Private.Update(argsCtx)
for idx, argValue := range args {
macroCtx.Private[node.argsOrder[idx]] = argValue.Interface()
}
var b bytes.Buffer
err := node.wrapper.Execute(macroCtx, &b)
if err != nil {
return AsSafeValue(err.updateFromTokenIfNeeded(ctx.template, node.position).Error())
}
return AsSafeValue(b.String())
}
func tagMacroParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
macroNode := &tagMacroNode{
position: start,
args: make(map[string]IEvaluator),
}
nameToken := arguments.MatchType(TokenIdentifier)
if nameToken == nil {
return nil, arguments.Error("Macro-tag needs at least an identifier as name.", nil)
}
macroNode.name = nameToken.Val
if arguments.MatchOne(TokenSymbol, "(") == nil {
return nil, arguments.Error("Expected '('.", nil)
}
for arguments.Match(TokenSymbol, ")") == nil {
argNameToken := arguments.MatchType(TokenIdentifier)
if argNameToken == nil {
return nil, arguments.Error("Expected argument name as identifier.", nil)
}
macroNode.argsOrder = append(macroNode.argsOrder, argNameToken.Val)
if arguments.Match(TokenSymbol, "=") != nil {
// Default expression follows
argDefaultExpr, err := arguments.ParseExpression()
if err != nil {
return nil, err
}
macroNode.args[argNameToken.Val] = argDefaultExpr
} else {
// No default expression
macroNode.args[argNameToken.Val] = nil
}
if arguments.Match(TokenSymbol, ")") != nil {
break
}
if arguments.Match(TokenSymbol, ",") == nil {
return nil, arguments.Error("Expected ',' or ')'.", nil)
}
}
if arguments.Match(TokenKeyword, "export") != nil {
macroNode.exported = true
}
if arguments.Remaining() > 0 {
return nil, arguments.Error("Malformed macro-tag.", nil)
}
// Body wrapping
wrapper, endargs, err := doc.WrapUntilTag("endmacro")
if err != nil {
return nil, err
}
macroNode.wrapper = wrapper
if endargs.Count() > 0 {
return nil, endargs.Error("Arguments not allowed here.", nil)
}
if macroNode.exported {
// Now register the macro if it wants to be exported
_, has := doc.template.exportedMacros[macroNode.name]
if has {
return nil, doc.Error(fmt.Sprintf("another macro with name '%s' already exported", macroNode.name), start)
}
doc.template.exportedMacros[macroNode.name] = macroNode
}
return macroNode, nil
}
func init() {
RegisterTag("macro", tagMacroParser)
}

View file

@ -1,50 +0,0 @@
package pongo2
import (
"time"
)
type tagNowNode struct {
position *Token
format string
fake bool
}
func (node *tagNowNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
var t time.Time
if node.fake {
t = time.Date(2014, time.February, 05, 18, 31, 45, 00, time.UTC)
} else {
t = time.Now()
}
writer.WriteString(t.Format(node.format))
return nil
}
func tagNowParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
nowNode := &tagNowNode{
position: start,
}
formatToken := arguments.MatchType(TokenString)
if formatToken == nil {
return nil, arguments.Error("Expected a format string.", nil)
}
nowNode.format = formatToken.Val
if arguments.MatchOne(TokenIdentifier, "fake") != nil {
nowNode.fake = true
}
if arguments.Remaining() > 0 {
return nil, arguments.Error("Malformed now-tag arguments.", nil)
}
return nowNode, nil
}
func init() {
RegisterTag("now", tagNowParser)
}

View file

@ -1,50 +0,0 @@
package pongo2
type tagSetNode struct {
name string
expression IEvaluator
}
func (node *tagSetNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
// Evaluate expression
value, err := node.expression.Evaluate(ctx)
if err != nil {
return err
}
ctx.Private[node.name] = value
return nil
}
func tagSetParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
node := &tagSetNode{}
// Parse variable name
typeToken := arguments.MatchType(TokenIdentifier)
if typeToken == nil {
return nil, arguments.Error("Expected an identifier.", nil)
}
node.name = typeToken.Val
if arguments.Match(TokenSymbol, "=") == nil {
return nil, arguments.Error("Expected '='.", nil)
}
// Variable expression
keyExpression, err := arguments.ParseExpression()
if err != nil {
return nil, err
}
node.expression = keyExpression
// Remaining arguments
if arguments.Remaining() > 0 {
return nil, arguments.Error("Malformed 'set'-tag arguments.", nil)
}
return node, nil
}
func init() {
RegisterTag("set", tagSetParser)
}

View file

@ -1,54 +0,0 @@
package pongo2
import (
"bytes"
"regexp"
)
type tagSpacelessNode struct {
wrapper *NodeWrapper
}
var tagSpacelessRegexp = regexp.MustCompile(`(?U:(<.*>))([\t\n\v\f\r ]+)(?U:(<.*>))`)
func (node *tagSpacelessNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
b := bytes.NewBuffer(make([]byte, 0, 1024)) // 1 KiB
err := node.wrapper.Execute(ctx, b)
if err != nil {
return err
}
s := b.String()
// Repeat this recursively
changed := true
for changed {
s2 := tagSpacelessRegexp.ReplaceAllString(s, "$1$3")
changed = s != s2
s = s2
}
writer.WriteString(s)
return nil
}
func tagSpacelessParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
spacelessNode := &tagSpacelessNode{}
wrapper, _, err := doc.WrapUntilTag("endspaceless")
if err != nil {
return nil, err
}
spacelessNode.wrapper = wrapper
if arguments.Remaining() > 0 {
return nil, arguments.Error("Malformed spaceless-tag arguments.", nil)
}
return spacelessNode, nil
}
func init() {
RegisterTag("spaceless", tagSpacelessParser)
}

View file

@ -1,68 +0,0 @@
package pongo2
import (
"io/ioutil"
)
type tagSSINode struct {
filename string
content string
template *Template
}
func (node *tagSSINode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
if node.template != nil {
// Execute the template within the current context
includeCtx := make(Context)
includeCtx.Update(ctx.Public)
includeCtx.Update(ctx.Private)
err := node.template.execute(includeCtx, writer)
if err != nil {
return err.(*Error)
}
} else {
// Just print out the content
writer.WriteString(node.content)
}
return nil
}
func tagSSIParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
SSINode := &tagSSINode{}
if fileToken := arguments.MatchType(TokenString); fileToken != nil {
SSINode.filename = fileToken.Val
if arguments.Match(TokenIdentifier, "parsed") != nil {
// parsed
temporaryTpl, err := doc.template.set.FromFile(doc.template.set.resolveFilename(doc.template, fileToken.Val))
if err != nil {
return nil, err.(*Error).updateFromTokenIfNeeded(doc.template, fileToken)
}
SSINode.template = temporaryTpl
} else {
// plaintext
buf, err := ioutil.ReadFile(doc.template.set.resolveFilename(doc.template, fileToken.Val))
if err != nil {
return nil, (&Error{
Sender: "tag:ssi",
OrigError: err,
}).updateFromTokenIfNeeded(doc.template, fileToken)
}
SSINode.content = string(buf)
}
} else {
return nil, arguments.Error("First argument must be a string.", nil)
}
if arguments.Remaining() > 0 {
return nil, arguments.Error("Malformed SSI-tag argument.", nil)
}
return SSINode, nil
}
func init() {
RegisterTag("ssi", tagSSIParser)
}

View file

@ -1,45 +0,0 @@
package pongo2
type tagTemplateTagNode struct {
content string
}
var templateTagMapping = map[string]string{
"openblock": "{%",
"closeblock": "%}",
"openvariable": "{{",
"closevariable": "}}",
"openbrace": "{",
"closebrace": "}",
"opencomment": "{#",
"closecomment": "#}",
}
func (node *tagTemplateTagNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
writer.WriteString(node.content)
return nil
}
func tagTemplateTagParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
ttNode := &tagTemplateTagNode{}
if argToken := arguments.MatchType(TokenIdentifier); argToken != nil {
output, found := templateTagMapping[argToken.Val]
if !found {
return nil, arguments.Error("Argument not found", argToken)
}
ttNode.content = output
} else {
return nil, arguments.Error("Identifier expected.", nil)
}
if arguments.Remaining() > 0 {
return nil, arguments.Error("Malformed templatetag-tag argument.", nil)
}
return ttNode, nil
}
func init() {
RegisterTag("templatetag", tagTemplateTagParser)
}

View file

@ -1,83 +0,0 @@
package pongo2
import (
"fmt"
"math"
)
type tagWidthratioNode struct {
position *Token
current, max IEvaluator
width IEvaluator
ctxName string
}
func (node *tagWidthratioNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
current, err := node.current.Evaluate(ctx)
if err != nil {
return err
}
max, err := node.max.Evaluate(ctx)
if err != nil {
return err
}
width, err := node.width.Evaluate(ctx)
if err != nil {
return err
}
value := int(math.Ceil(current.Float()/max.Float()*width.Float() + 0.5))
if node.ctxName == "" {
writer.WriteString(fmt.Sprintf("%d", value))
} else {
ctx.Private[node.ctxName] = value
}
return nil
}
func tagWidthratioParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
widthratioNode := &tagWidthratioNode{
position: start,
}
current, err := arguments.ParseExpression()
if err != nil {
return nil, err
}
widthratioNode.current = current
max, err := arguments.ParseExpression()
if err != nil {
return nil, err
}
widthratioNode.max = max
width, err := arguments.ParseExpression()
if err != nil {
return nil, err
}
widthratioNode.width = width
if arguments.MatchOne(TokenKeyword, "as") != nil {
// Name follows
nameToken := arguments.MatchType(TokenIdentifier)
if nameToken == nil {
return nil, arguments.Error("Expected name (identifier).", nil)
}
widthratioNode.ctxName = nameToken.Val
}
if arguments.Remaining() > 0 {
return nil, arguments.Error("Malformed widthratio-tag arguments.", nil)
}
return widthratioNode, nil
}
func init() {
RegisterTag("widthratio", tagWidthratioParser)
}

View file

@ -1,88 +0,0 @@
package pongo2
type tagWithNode struct {
withPairs map[string]IEvaluator
wrapper *NodeWrapper
}
func (node *tagWithNode) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
//new context for block
withctx := NewChildExecutionContext(ctx)
// Put all custom with-pairs into the context
for key, value := range node.withPairs {
val, err := value.Evaluate(ctx)
if err != nil {
return err
}
withctx.Private[key] = val
}
return node.wrapper.Execute(withctx, writer)
}
func tagWithParser(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) {
withNode := &tagWithNode{
withPairs: make(map[string]IEvaluator),
}
if arguments.Count() == 0 {
return nil, arguments.Error("Tag 'with' requires at least one argument.", nil)
}
wrapper, endargs, err := doc.WrapUntilTag("endwith")
if err != nil {
return nil, err
}
withNode.wrapper = wrapper
if endargs.Count() > 0 {
return nil, endargs.Error("Arguments not allowed here.", nil)
}
// Scan through all arguments to see which style the user uses (old or new style).
// If we find any "as" keyword we will enforce old style; otherwise we will use new style.
oldStyle := false // by default we're using the new_style
for i := 0; i < arguments.Count(); i++ {
if arguments.PeekN(i, TokenKeyword, "as") != nil {
oldStyle = true
break
}
}
for arguments.Remaining() > 0 {
if oldStyle {
valueExpr, err := arguments.ParseExpression()
if err != nil {
return nil, err
}
if arguments.Match(TokenKeyword, "as") == nil {
return nil, arguments.Error("Expected 'as' keyword.", nil)
}
keyToken := arguments.MatchType(TokenIdentifier)
if keyToken == nil {
return nil, arguments.Error("Expected an identifier", nil)
}
withNode.withPairs[keyToken.Val] = valueExpr
} else {
keyToken := arguments.MatchType(TokenIdentifier)
if keyToken == nil {
return nil, arguments.Error("Expected an identifier", nil)
}
if arguments.Match(TokenSymbol, "=") == nil {
return nil, arguments.Error("Expected '='.", nil)
}
valueExpr, err := arguments.ParseExpression()
if err != nil {
return nil, err
}
withNode.withPairs[keyToken.Val] = valueExpr
}
}
return withNode, nil
}
func init() {
RegisterTag("with", tagWithParser)
}

View file

@ -1,276 +0,0 @@
package pongo2
import (
"bytes"
"fmt"
"io"
"strings"
)
type TemplateWriter interface {
io.Writer
WriteString(string) (int, error)
}
type templateWriter struct {
w io.Writer
}
func (tw *templateWriter) WriteString(s string) (int, error) {
return tw.w.Write([]byte(s))
}
func (tw *templateWriter) Write(b []byte) (int, error) {
return tw.w.Write(b)
}
type Template struct {
set *TemplateSet
// Input
isTplString bool
name string
tpl string
size int
// Calculation
tokens []*Token
parser *Parser
// first come, first serve (it's important to not override existing entries in here)
level int
parent *Template
child *Template
blocks map[string]*NodeWrapper
exportedMacros map[string]*tagMacroNode
// Output
root *nodeDocument
// Options allow you to change the behavior of template-engine.
// You can change the options before calling the Execute method.
Options *Options
}
func newTemplateString(set *TemplateSet, tpl []byte) (*Template, error) {
return newTemplate(set, "<string>", true, tpl)
}
func newTemplate(set *TemplateSet, name string, isTplString bool, tpl []byte) (*Template, error) {
strTpl := string(tpl)
// Create the template
t := &Template{
set: set,
isTplString: isTplString,
name: name,
tpl: strTpl,
size: len(strTpl),
blocks: make(map[string]*NodeWrapper),
exportedMacros: make(map[string]*tagMacroNode),
Options: newOptions(),
}
// Copy all settings from another Options.
t.Options.Update(set.Options)
// Tokenize it
tokens, err := lex(name, strTpl)
if err != nil {
return nil, err
}
t.tokens = tokens
// For debugging purposes, show all tokens:
/*for i, t := range tokens {
fmt.Printf("%3d. %s\n", i, t)
}*/
// Parse it
err = t.parse()
if err != nil {
return nil, err
}
return t, nil
}
func (tpl *Template) newContextForExecution(context Context) (*Template, *ExecutionContext, error) {
if tpl.Options.TrimBlocks || tpl.Options.LStripBlocks {
// Issue #94 https://github.com/flosch/pongo2/issues/94
// If an application configures pongo2 template to trim_blocks,
// the first newline after a template tag is removed automatically (like in PHP).
prev := &Token{
Typ: TokenHTML,
Val: "\n",
}
for _, t := range tpl.tokens {
if tpl.Options.LStripBlocks {
if prev.Typ == TokenHTML && t.Typ != TokenHTML && t.Val == "{%" {
prev.Val = strings.TrimRight(prev.Val, "\t ")
}
}
if tpl.Options.TrimBlocks {
if prev.Typ != TokenHTML && t.Typ == TokenHTML && prev.Val == "%}" {
if len(t.Val) > 0 && t.Val[0] == '\n' {
t.Val = t.Val[1:len(t.Val)]
}
}
}
prev = t
}
}
// Determine the parent to be executed (for template inheritance)
parent := tpl
for parent.parent != nil {
parent = parent.parent
}
// Create context if none is given
newContext := make(Context)
newContext.Update(tpl.set.Globals)
if context != nil {
newContext.Update(context)
if len(newContext) > 0 {
// Check for context name syntax
err := newContext.checkForValidIdentifiers()
if err != nil {
return parent, nil, err
}
// Check for clashes with macro names
for k := range newContext {
_, has := tpl.exportedMacros[k]
if has {
return parent, nil, &Error{
Filename: tpl.name,
Sender: "execution",
OrigError: fmt.Errorf("context key name '%s' clashes with macro '%s'", k, k),
}
}
}
}
}
// Create operational context
ctx := newExecutionContext(parent, newContext)
return parent, ctx, nil
}
func (tpl *Template) execute(context Context, writer TemplateWriter) error {
parent, ctx, err := tpl.newContextForExecution(context)
if err != nil {
return err
}
// Run the selected document
if err := parent.root.Execute(ctx, writer); err != nil {
return err
}
return nil
}
func (tpl *Template) newTemplateWriterAndExecute(context Context, writer io.Writer) error {
return tpl.execute(context, &templateWriter{w: writer})
}
func (tpl *Template) newBufferAndExecute(context Context) (*bytes.Buffer, error) {
// Create output buffer
// We assume that the rendered template will be 30% larger
buffer := bytes.NewBuffer(make([]byte, 0, int(float64(tpl.size)*1.3)))
if err := tpl.execute(context, buffer); err != nil {
return nil, err
}
return buffer, nil
}
// Executes the template with the given context and writes to writer (io.Writer)
// on success. Context can be nil. Nothing is written on error; instead the error
// is being returned.
func (tpl *Template) ExecuteWriter(context Context, writer io.Writer) error {
buf, err := tpl.newBufferAndExecute(context)
if err != nil {
return err
}
_, err = buf.WriteTo(writer)
if err != nil {
return err
}
return nil
}
// Same as ExecuteWriter. The only difference between both functions is that
// this function might already have written parts of the generated template in the
// case of an execution error because there's no intermediate buffer involved for
// performance reasons. This is handy if you need high performance template
// generation or if you want to manage your own pool of buffers.
func (tpl *Template) ExecuteWriterUnbuffered(context Context, writer io.Writer) error {
return tpl.newTemplateWriterAndExecute(context, writer)
}
// Executes the template and returns the rendered template as a []byte
func (tpl *Template) ExecuteBytes(context Context) ([]byte, error) {
// Execute template
buffer, err := tpl.newBufferAndExecute(context)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
// Executes the template and returns the rendered template as a string
func (tpl *Template) Execute(context Context) (string, error) {
// Execute template
buffer, err := tpl.newBufferAndExecute(context)
if err != nil {
return "", err
}
return buffer.String(), nil
}
func (tpl *Template) ExecuteBlocks(context Context, blocks []string) (map[string]string, error) {
var parents []*Template
result := make(map[string]string)
parent := tpl
for parent != nil {
parents = append(parents, parent)
parent = parent.parent
}
for _, t := range parents {
buffer := bytes.NewBuffer(make([]byte, 0, int(float64(t.size)*1.3)))
_, ctx, err := t.newContextForExecution(context)
if err != nil {
return nil, err
}
for _, blockName := range blocks {
if _, ok := result[blockName]; ok {
continue
}
if blockWrapper, ok := t.blocks[blockName]; ok {
bErr := blockWrapper.Execute(ctx, buffer)
if bErr != nil {
return nil, bErr
}
result[blockName] = buffer.String()
buffer.Reset()
}
}
// We have found all blocks
if len(blocks) == len(result) {
break
}
}
return result, nil
}

View file

@ -1,156 +0,0 @@
package pongo2
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
)
// LocalFilesystemLoader represents a local filesystem loader with basic
// BaseDirectory capabilities. The access to the local filesystem is unrestricted.
type LocalFilesystemLoader struct {
baseDir string
}
// MustNewLocalFileSystemLoader creates a new LocalFilesystemLoader instance
// and panics if there's any error during instantiation. The parameters
// are the same like NewLocalFileSystemLoader.
func MustNewLocalFileSystemLoader(baseDir string) *LocalFilesystemLoader {
fs, err := NewLocalFileSystemLoader(baseDir)
if err != nil {
log.Panic(err)
}
return fs
}
// NewLocalFileSystemLoader creates a new LocalFilesystemLoader and allows
// templatesto be loaded from disk (unrestricted). If any base directory
// is given (or being set using SetBaseDir), this base directory is being used
// for path calculation in template inclusions/imports. Otherwise the path
// is calculated based relatively to the including template's path.
func NewLocalFileSystemLoader(baseDir string) (*LocalFilesystemLoader, error) {
fs := &LocalFilesystemLoader{}
if baseDir != "" {
if err := fs.SetBaseDir(baseDir); err != nil {
return nil, err
}
}
return fs, nil
}
// SetBaseDir sets the template's base directory. This directory will
// be used for any relative path in filters, tags and From*-functions to determine
// your template. See the comment for NewLocalFileSystemLoader as well.
func (fs *LocalFilesystemLoader) SetBaseDir(path string) error {
// Make the path absolute
if !filepath.IsAbs(path) {
abs, err := filepath.Abs(path)
if err != nil {
return err
}
path = abs
}
// Check for existence
fi, err := os.Stat(path)
if err != nil {
return err
}
if !fi.IsDir() {
return fmt.Errorf("The given path '%s' is not a directory.", path)
}
fs.baseDir = path
return nil
}
// Get reads the path's content from your local filesystem.
func (fs *LocalFilesystemLoader) Get(path string) (io.Reader, error) {
buf, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
return bytes.NewReader(buf), nil
}
// Abs resolves a filename relative to the base directory. Absolute paths are allowed.
// When there's no base dir set, the absolute path to the filename
// will be calculated based on either the provided base directory (which
// might be a path of a template which includes another template) or
// the current working directory.
func (fs *LocalFilesystemLoader) Abs(base, name string) string {
if filepath.IsAbs(name) {
return name
}
// Our own base dir has always priority; if there's none
// we use the path provided in base.
var err error
if fs.baseDir == "" {
if base == "" {
base, err = os.Getwd()
if err != nil {
panic(err)
}
return filepath.Join(base, name)
}
return filepath.Join(filepath.Dir(base), name)
}
return filepath.Join(fs.baseDir, name)
}
// SandboxedFilesystemLoader is still WIP.
type SandboxedFilesystemLoader struct {
*LocalFilesystemLoader
}
// NewSandboxedFilesystemLoader creates a new sandboxed local file system instance.
func NewSandboxedFilesystemLoader(baseDir string) (*SandboxedFilesystemLoader, error) {
fs, err := NewLocalFileSystemLoader(baseDir)
if err != nil {
return nil, err
}
return &SandboxedFilesystemLoader{
LocalFilesystemLoader: fs,
}, nil
}
// Move sandbox to a virtual fs
/*
if len(set.SandboxDirectories) > 0 {
defer func() {
// Remove any ".." or other crap
resolvedPath = filepath.Clean(resolvedPath)
// Make the path absolute
absPath, err := filepath.Abs(resolvedPath)
if err != nil {
panic(err)
}
resolvedPath = absPath
// Check against the sandbox directories (once one pattern matches, we're done and can allow it)
for _, pattern := range set.SandboxDirectories {
matched, err := filepath.Match(pattern, resolvedPath)
if err != nil {
panic("Wrong sandbox directory match pattern (see http://golang.org/pkg/path/filepath/#Match).")
}
if matched {
// OK!
return
}
}
// No pattern matched, we have to log+deny the request
set.logf("Access attempt outside of the sandbox directories (blocked): '%s'", resolvedPath)
resolvedPath = ""
}()
}
*/

View file

@ -1,305 +0,0 @@
package pongo2
import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
"sync"
"errors"
)
// TemplateLoader allows to implement a virtual file system.
type TemplateLoader interface {
// Abs calculates the path to a given template. Whenever a path must be resolved
// due to an import from another template, the base equals the parent template's path.
Abs(base, name string) string
// Get returns an io.Reader where the template's content can be read from.
Get(path string) (io.Reader, error)
}
// TemplateSet allows you to create your own group of templates with their own
// global context (which is shared among all members of the set) and their own
// configuration.
// It's useful for a separation of different kind of templates
// (e. g. web templates vs. mail templates).
type TemplateSet struct {
name string
loaders []TemplateLoader
// Globals will be provided to all templates created within this template set
Globals Context
// If debug is true (default false), ExecutionContext.Logf() will work and output
// to STDOUT. Furthermore, FromCache() won't cache the templates.
// Make sure to synchronize the access to it in case you're changing this
// variable during program execution (and template compilation/execution).
Debug bool
// Options allow you to change the behavior of template-engine.
// You can change the options before calling the Execute method.
Options *Options
// Sandbox features
// - Disallow access to specific tags and/or filters (using BanTag() and BanFilter())
//
// For efficiency reasons you can ban tags/filters only *before* you have
// added your first template to the set (restrictions are statically checked).
// After you added one, it's not possible anymore (for your personal security).
firstTemplateCreated bool
bannedTags map[string]bool
bannedFilters map[string]bool
// Template cache (for FromCache())
templateCache map[string]*Template
templateCacheMutex sync.Mutex
}
// NewSet can be used to create sets with different kind of templates
// (e. g. web from mail templates), with different globals or
// other configurations.
func NewSet(name string, loaders ...TemplateLoader) *TemplateSet {
if len(loaders) == 0 {
panic(fmt.Errorf("at least one template loader must be specified"))
}
return &TemplateSet{
name: name,
loaders: loaders,
Globals: make(Context),
bannedTags: make(map[string]bool),
bannedFilters: make(map[string]bool),
templateCache: make(map[string]*Template),
Options: newOptions(),
}
}
func (set *TemplateSet) AddLoader(loaders ...TemplateLoader) {
set.loaders = append(set.loaders, loaders...)
}
func (set *TemplateSet) resolveFilename(tpl *Template, path string) string {
return set.resolveFilenameForLoader(set.loaders[0], tpl, path)
}
func (set *TemplateSet) resolveFilenameForLoader(loader TemplateLoader, tpl *Template, path string) string {
name := ""
if tpl != nil && tpl.isTplString {
return path
}
if tpl != nil {
name = tpl.name
}
return loader.Abs(name, path)
}
// BanTag bans a specific tag for this template set. See more in the documentation for TemplateSet.
func (set *TemplateSet) BanTag(name string) error {
_, has := tags[name]
if !has {
return fmt.Errorf("tag '%s' not found", name)
}
if set.firstTemplateCreated {
return errors.New("you cannot ban any tags after you've added your first template to your template set")
}
_, has = set.bannedTags[name]
if has {
return fmt.Errorf("tag '%s' is already banned", name)
}
set.bannedTags[name] = true
return nil
}
// BanFilter bans a specific filter for this template set. See more in the documentation for TemplateSet.
func (set *TemplateSet) BanFilter(name string) error {
_, has := filters[name]
if !has {
return fmt.Errorf("filter '%s' not found", name)
}
if set.firstTemplateCreated {
return errors.New("you cannot ban any filters after you've added your first template to your template set")
}
_, has = set.bannedFilters[name]
if has {
return fmt.Errorf("filter '%s' is already banned", name)
}
set.bannedFilters[name] = true
return nil
}
func (set *TemplateSet) resolveTemplate(tpl *Template, path string) (name string, loader TemplateLoader, fd io.Reader, err error) {
// iterate over loaders until we appear to have a valid template
for _, loader = range set.loaders {
name = set.resolveFilenameForLoader(loader, tpl, path)
fd, err = loader.Get(name)
if err == nil {
return
}
}
return path, nil, nil, fmt.Errorf("unable to resolve template")
}
// CleanCache cleans the template cache. If filenames is not empty,
// it will remove the template caches of those filenames.
// Or it will empty the whole template cache. It is thread-safe.
func (set *TemplateSet) CleanCache(filenames ...string) {
set.templateCacheMutex.Lock()
defer set.templateCacheMutex.Unlock()
if len(filenames) == 0 {
set.templateCache = make(map[string]*Template, len(set.templateCache))
}
for _, filename := range filenames {
delete(set.templateCache, set.resolveFilename(nil, filename))
}
}
// FromCache is a convenient method to cache templates. It is thread-safe
// and will only compile the template associated with a filename once.
// If TemplateSet.Debug is true (for example during development phase),
// FromCache() will not cache the template and instead recompile it on any
// call (to make changes to a template live instantaneously).
func (set *TemplateSet) FromCache(filename string) (*Template, error) {
if set.Debug {
// Recompile on any request
return set.FromFile(filename)
}
// Cache the template
cleanedFilename := set.resolveFilename(nil, filename)
set.templateCacheMutex.Lock()
defer set.templateCacheMutex.Unlock()
tpl, has := set.templateCache[cleanedFilename]
// Cache miss
if !has {
tpl, err := set.FromFile(cleanedFilename)
if err != nil {
return nil, err
}
set.templateCache[cleanedFilename] = tpl
return tpl, nil
}
// Cache hit
return tpl, nil
}
// FromString loads a template from string and returns a Template instance.
func (set *TemplateSet) FromString(tpl string) (*Template, error) {
set.firstTemplateCreated = true
return newTemplateString(set, []byte(tpl))
}
// FromBytes loads a template from bytes and returns a Template instance.
func (set *TemplateSet) FromBytes(tpl []byte) (*Template, error) {
set.firstTemplateCreated = true
return newTemplateString(set, tpl)
}
// FromFile loads a template from a filename and returns a Template instance.
func (set *TemplateSet) FromFile(filename string) (*Template, error) {
set.firstTemplateCreated = true
_, _, fd, err := set.resolveTemplate(nil, filename)
if err != nil {
return nil, &Error{
Filename: filename,
Sender: "fromfile",
OrigError: err,
}
}
buf, err := ioutil.ReadAll(fd)
if err != nil {
return nil, &Error{
Filename: filename,
Sender: "fromfile",
OrigError: err,
}
}
return newTemplate(set, filename, false, buf)
}
// RenderTemplateString is a shortcut and renders a template string directly.
func (set *TemplateSet) RenderTemplateString(s string, ctx Context) (string, error) {
set.firstTemplateCreated = true
tpl := Must(set.FromString(s))
result, err := tpl.Execute(ctx)
if err != nil {
return "", err
}
return result, nil
}
// RenderTemplateBytes is a shortcut and renders template bytes directly.
func (set *TemplateSet) RenderTemplateBytes(b []byte, ctx Context) (string, error) {
set.firstTemplateCreated = true
tpl := Must(set.FromBytes(b))
result, err := tpl.Execute(ctx)
if err != nil {
return "", err
}
return result, nil
}
// RenderTemplateFile is a shortcut and renders a template file directly.
func (set *TemplateSet) RenderTemplateFile(fn string, ctx Context) (string, error) {
set.firstTemplateCreated = true
tpl := Must(set.FromFile(fn))
result, err := tpl.Execute(ctx)
if err != nil {
return "", err
}
return result, nil
}
func (set *TemplateSet) logf(format string, args ...interface{}) {
if set.Debug {
logger.Printf(fmt.Sprintf("[template set: %s] %s", set.name, format), args...)
}
}
// Logging function (internally used)
func logf(format string, items ...interface{}) {
if debug {
logger.Printf(format, items...)
}
}
var (
debug bool // internal debugging
logger = log.New(os.Stdout, "[pongo2] ", log.LstdFlags|log.Lshortfile)
// DefaultLoader allows the default un-sandboxed access to the local file
// system and is being used by the DefaultSet.
DefaultLoader = MustNewLocalFileSystemLoader("")
// DefaultSet is a set created for you for convinience reasons.
DefaultSet = NewSet("default", DefaultLoader)
// Methods on the default set
FromString = DefaultSet.FromString
FromBytes = DefaultSet.FromBytes
FromFile = DefaultSet.FromFile
FromCache = DefaultSet.FromCache
RenderTemplateString = DefaultSet.RenderTemplateString
RenderTemplateFile = DefaultSet.RenderTemplateFile
// Globals for the default set
Globals = DefaultSet.Globals
)

View file

@ -1,540 +0,0 @@
package pongo2
import (
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"time"
)
type Value struct {
val reflect.Value
safe bool // used to indicate whether a Value needs explicit escaping in the template
}
// AsValue converts any given value to a pongo2.Value
// Usually being used within own functions passed to a template
// through a Context or within filter functions.
//
// Example:
// AsValue("my string")
func AsValue(i interface{}) *Value {
return &Value{
val: reflect.ValueOf(i),
}
}
// AsSafeValue works like AsValue, but does not apply the 'escape' filter.
func AsSafeValue(i interface{}) *Value {
return &Value{
val: reflect.ValueOf(i),
safe: true,
}
}
func (v *Value) getResolvedValue() reflect.Value {
if v.val.IsValid() && v.val.Kind() == reflect.Ptr {
return v.val.Elem()
}
return v.val
}
// IsString checks whether the underlying value is a string
func (v *Value) IsString() bool {
return v.getResolvedValue().Kind() == reflect.String
}
// IsBool checks whether the underlying value is a bool
func (v *Value) IsBool() bool {
return v.getResolvedValue().Kind() == reflect.Bool
}
// IsFloat checks whether the underlying value is a float
func (v *Value) IsFloat() bool {
return v.getResolvedValue().Kind() == reflect.Float32 ||
v.getResolvedValue().Kind() == reflect.Float64
}
// IsInteger checks whether the underlying value is an integer
func (v *Value) IsInteger() bool {
return v.getResolvedValue().Kind() == reflect.Int ||
v.getResolvedValue().Kind() == reflect.Int8 ||
v.getResolvedValue().Kind() == reflect.Int16 ||
v.getResolvedValue().Kind() == reflect.Int32 ||
v.getResolvedValue().Kind() == reflect.Int64 ||
v.getResolvedValue().Kind() == reflect.Uint ||
v.getResolvedValue().Kind() == reflect.Uint8 ||
v.getResolvedValue().Kind() == reflect.Uint16 ||
v.getResolvedValue().Kind() == reflect.Uint32 ||
v.getResolvedValue().Kind() == reflect.Uint64
}
// IsNumber checks whether the underlying value is either an integer
// or a float.
func (v *Value) IsNumber() bool {
return v.IsInteger() || v.IsFloat()
}
// IsTime checks whether the underlying value is a time.Time.
func (v *Value) IsTime() bool {
_, ok := v.Interface().(time.Time)
return ok
}
// IsNil checks whether the underlying value is NIL
func (v *Value) IsNil() bool {
//fmt.Printf("%+v\n", v.getResolvedValue().Type().String())
return !v.getResolvedValue().IsValid()
}
// String returns a string for the underlying value. If this value is not
// of type string, pongo2 tries to convert it. Currently the following
// types for underlying values are supported:
//
// 1. string
// 2. int/uint (any size)
// 3. float (any precision)
// 4. bool
// 5. time.Time
// 6. String() will be called on the underlying value if provided
//
// NIL values will lead to an empty string. Unsupported types are leading
// to their respective type name.
func (v *Value) String() string {
if v.IsNil() {
return ""
}
switch v.getResolvedValue().Kind() {
case reflect.String:
return v.getResolvedValue().String()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.FormatInt(v.getResolvedValue().Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return strconv.FormatUint(v.getResolvedValue().Uint(), 10)
case reflect.Float32, reflect.Float64:
return fmt.Sprintf("%f", v.getResolvedValue().Float())
case reflect.Bool:
if v.Bool() {
return "True"
}
return "False"
case reflect.Struct:
if t, ok := v.Interface().(fmt.Stringer); ok {
return t.String()
}
}
logf("Value.String() not implemented for type: %s\n", v.getResolvedValue().Kind().String())
return v.getResolvedValue().String()
}
// Integer returns the underlying value as an integer (converts the underlying
// value, if necessary). If it's not possible to convert the underlying value,
// it will return 0.
func (v *Value) Integer() int {
switch v.getResolvedValue().Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return int(v.getResolvedValue().Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return int(v.getResolvedValue().Uint())
case reflect.Float32, reflect.Float64:
return int(v.getResolvedValue().Float())
case reflect.String:
// Try to convert from string to int (base 10)
f, err := strconv.ParseFloat(v.getResolvedValue().String(), 64)
if err != nil {
return 0
}
return int(f)
default:
logf("Value.Integer() not available for type: %s\n", v.getResolvedValue().Kind().String())
return 0
}
}
// Float returns the underlying value as a float (converts the underlying
// value, if necessary). If it's not possible to convert the underlying value,
// it will return 0.0.
func (v *Value) Float() float64 {
switch v.getResolvedValue().Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return float64(v.getResolvedValue().Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return float64(v.getResolvedValue().Uint())
case reflect.Float32, reflect.Float64:
return v.getResolvedValue().Float()
case reflect.String:
// Try to convert from string to float64 (base 10)
f, err := strconv.ParseFloat(v.getResolvedValue().String(), 64)
if err != nil {
return 0.0
}
return f
default:
logf("Value.Float() not available for type: %s\n", v.getResolvedValue().Kind().String())
return 0.0
}
}
// Bool returns the underlying value as bool. If the value is not bool, false
// will always be returned. If you're looking for true/false-evaluation of the
// underlying value, have a look on the IsTrue()-function.
func (v *Value) Bool() bool {
switch v.getResolvedValue().Kind() {
case reflect.Bool:
return v.getResolvedValue().Bool()
default:
logf("Value.Bool() not available for type: %s\n", v.getResolvedValue().Kind().String())
return false
}
}
// Time returns the underlying value as time.Time.
// If the underlying value is not a time.Time, it returns the zero value of time.Time.
func (v *Value) Time() time.Time {
tm, ok := v.Interface().(time.Time)
if ok {
return tm
}
return time.Time{}
}
// IsTrue tries to evaluate the underlying value the Pythonic-way:
//
// Returns TRUE in one the following cases:
//
// * int != 0
// * uint != 0
// * float != 0.0
// * len(array/chan/map/slice/string) > 0
// * bool == true
// * underlying value is a struct
//
// Otherwise returns always FALSE.
func (v *Value) IsTrue() bool {
switch v.getResolvedValue().Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.getResolvedValue().Int() != 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return v.getResolvedValue().Uint() != 0
case reflect.Float32, reflect.Float64:
return v.getResolvedValue().Float() != 0
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String:
return v.getResolvedValue().Len() > 0
case reflect.Bool:
return v.getResolvedValue().Bool()
case reflect.Struct:
return true // struct instance is always true
default:
logf("Value.IsTrue() not available for type: %s\n", v.getResolvedValue().Kind().String())
return false
}
}
// Negate tries to negate the underlying value. It's mainly used for
// the NOT-operator and in conjunction with a call to
// return_value.IsTrue() afterwards.
//
// Example:
// AsValue(1).Negate().IsTrue() == false
func (v *Value) Negate() *Value {
switch v.getResolvedValue().Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if v.Integer() != 0 {
return AsValue(0)
}
return AsValue(1)
case reflect.Float32, reflect.Float64:
if v.Float() != 0.0 {
return AsValue(float64(0.0))
}
return AsValue(float64(1.1))
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String:
return AsValue(v.getResolvedValue().Len() == 0)
case reflect.Bool:
return AsValue(!v.getResolvedValue().Bool())
case reflect.Struct:
return AsValue(false)
default:
logf("Value.IsTrue() not available for type: %s\n", v.getResolvedValue().Kind().String())
return AsValue(true)
}
}
// Len returns the length for an array, chan, map, slice or string.
// Otherwise it will return 0.
func (v *Value) Len() int {
switch v.getResolvedValue().Kind() {
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
return v.getResolvedValue().Len()
case reflect.String:
runes := []rune(v.getResolvedValue().String())
return len(runes)
default:
logf("Value.Len() not available for type: %s\n", v.getResolvedValue().Kind().String())
return 0
}
}
// Slice slices an array, slice or string. Otherwise it will
// return an empty []int.
func (v *Value) Slice(i, j int) *Value {
switch v.getResolvedValue().Kind() {
case reflect.Array, reflect.Slice:
return AsValue(v.getResolvedValue().Slice(i, j).Interface())
case reflect.String:
runes := []rune(v.getResolvedValue().String())
return AsValue(string(runes[i:j]))
default:
logf("Value.Slice() not available for type: %s\n", v.getResolvedValue().Kind().String())
return AsValue([]int{})
}
}
// Index gets the i-th item of an array, slice or string. Otherwise
// it will return NIL.
func (v *Value) Index(i int) *Value {
switch v.getResolvedValue().Kind() {
case reflect.Array, reflect.Slice:
if i >= v.Len() {
return AsValue(nil)
}
return AsValue(v.getResolvedValue().Index(i).Interface())
case reflect.String:
//return AsValue(v.getResolvedValue().Slice(i, i+1).Interface())
s := v.getResolvedValue().String()
runes := []rune(s)
if i < len(runes) {
return AsValue(string(runes[i]))
}
return AsValue("")
default:
logf("Value.Slice() not available for type: %s\n", v.getResolvedValue().Kind().String())
return AsValue([]int{})
}
}
// Contains checks whether the underlying value (which must be of type struct, map,
// string, array or slice) contains of another Value (e. g. used to check
// whether a struct contains of a specific field or a map contains a specific key).
//
// Example:
// AsValue("Hello, World!").Contains(AsValue("World")) == true
func (v *Value) Contains(other *Value) bool {
switch v.getResolvedValue().Kind() {
case reflect.Struct:
fieldValue := v.getResolvedValue().FieldByName(other.String())
return fieldValue.IsValid()
case reflect.Map:
var mapValue reflect.Value
switch other.Interface().(type) {
case int:
mapValue = v.getResolvedValue().MapIndex(other.getResolvedValue())
case string:
mapValue = v.getResolvedValue().MapIndex(other.getResolvedValue())
default:
logf("Value.Contains() does not support lookup type '%s'\n", other.getResolvedValue().Kind().String())
return false
}
return mapValue.IsValid()
case reflect.String:
return strings.Contains(v.getResolvedValue().String(), other.String())
case reflect.Slice, reflect.Array:
for i := 0; i < v.getResolvedValue().Len(); i++ {
item := v.getResolvedValue().Index(i)
if other.Interface() == item.Interface() {
return true
}
}
return false
default:
logf("Value.Contains() not available for type: %s\n", v.getResolvedValue().Kind().String())
return false
}
}
// CanSlice checks whether the underlying value is of type array, slice or string.
// You normally would use CanSlice() before using the Slice() operation.
func (v *Value) CanSlice() bool {
switch v.getResolvedValue().Kind() {
case reflect.Array, reflect.Slice, reflect.String:
return true
}
return false
}
// Iterate iterates over a map, array, slice or a string. It calls the
// function's first argument for every value with the following arguments:
//
// idx current 0-index
// count total amount of items
// key *Value for the key or item
// value *Value (only for maps, the respective value for a specific key)
//
// If the underlying value has no items or is not one of the types above,
// the empty function (function's second argument) will be called.
func (v *Value) Iterate(fn func(idx, count int, key, value *Value) bool, empty func()) {
v.IterateOrder(fn, empty, false, false)
}
// IterateOrder behaves like Value.Iterate, but can iterate through an array/slice/string in reverse. Does
// not affect the iteration through a map because maps don't have any particular order.
// However, you can force an order using the `sorted` keyword (and even use `reversed sorted`).
func (v *Value) IterateOrder(fn func(idx, count int, key, value *Value) bool, empty func(), reverse bool, sorted bool) {
switch v.getResolvedValue().Kind() {
case reflect.Map:
keys := sortedKeys(v.getResolvedValue().MapKeys())
if sorted {
if reverse {
sort.Sort(sort.Reverse(keys))
} else {
sort.Sort(keys)
}
}
keyLen := len(keys)
for idx, key := range keys {
value := v.getResolvedValue().MapIndex(key)
if !fn(idx, keyLen, &Value{val: key}, &Value{val: value}) {
return
}
}
if keyLen == 0 {
empty()
}
return // done
case reflect.Array, reflect.Slice:
var items valuesList
itemCount := v.getResolvedValue().Len()
for i := 0; i < itemCount; i++ {
items = append(items, &Value{val: v.getResolvedValue().Index(i)})
}
if sorted {
if reverse {
sort.Sort(sort.Reverse(items))
} else {
sort.Sort(items)
}
} else {
if reverse {
for i := 0; i < itemCount/2; i++ {
items[i], items[itemCount-1-i] = items[itemCount-1-i], items[i]
}
}
}
if len(items) > 0 {
for idx, item := range items {
if !fn(idx, itemCount, item, nil) {
return
}
}
} else {
empty()
}
return // done
case reflect.String:
if sorted {
// TODO(flosch): Handle sorted
panic("TODO: handle sort for type string")
}
// TODO(flosch): Not utf8-compatible (utf8-decoding necessary)
charCount := v.getResolvedValue().Len()
if charCount > 0 {
if reverse {
for i := charCount - 1; i >= 0; i-- {
if !fn(i, charCount, &Value{val: v.getResolvedValue().Slice(i, i+1)}, nil) {
return
}
}
} else {
for i := 0; i < charCount; i++ {
if !fn(i, charCount, &Value{val: v.getResolvedValue().Slice(i, i+1)}, nil) {
return
}
}
}
} else {
empty()
}
return // done
default:
logf("Value.Iterate() not available for type: %s\n", v.getResolvedValue().Kind().String())
}
empty()
}
// Interface gives you access to the underlying value.
func (v *Value) Interface() interface{} {
if v.val.IsValid() {
return v.val.Interface()
}
return nil
}
// EqualValueTo checks whether two values are containing the same value or object.
func (v *Value) EqualValueTo(other *Value) bool {
// comparison of uint with int fails using .Interface()-comparison (see issue #64)
if v.IsInteger() && other.IsInteger() {
return v.Integer() == other.Integer()
}
if v.IsTime() && other.IsTime() {
return v.Time().Equal(other.Time())
}
return v.Interface() == other.Interface()
}
type sortedKeys []reflect.Value
func (sk sortedKeys) Len() int {
return len(sk)
}
func (sk sortedKeys) Less(i, j int) bool {
vi := &Value{val: sk[i]}
vj := &Value{val: sk[j]}
switch {
case vi.IsInteger() && vj.IsInteger():
return vi.Integer() < vj.Integer()
case vi.IsFloat() && vj.IsFloat():
return vi.Float() < vj.Float()
default:
return vi.String() < vj.String()
}
}
func (sk sortedKeys) Swap(i, j int) {
sk[i], sk[j] = sk[j], sk[i]
}
type valuesList []*Value
func (vl valuesList) Len() int {
return len(vl)
}
func (vl valuesList) Less(i, j int) bool {
vi := vl[i]
vj := vl[j]
switch {
case vi.IsInteger() && vj.IsInteger():
return vi.Integer() < vj.Integer()
case vi.IsFloat() && vj.IsFloat():
return vi.Float() < vj.Float()
default:
return vi.String() < vj.String()
}
}
func (vl valuesList) Swap(i, j int) {
vl[i], vl[j] = vl[j], vl[i]
}

View file

@ -1,693 +0,0 @@
package pongo2
import (
"fmt"
"reflect"
"strconv"
"strings"
)
const (
varTypeInt = iota
varTypeIdent
)
var (
typeOfValuePtr = reflect.TypeOf(new(Value))
typeOfExecCtxPtr = reflect.TypeOf(new(ExecutionContext))
)
type variablePart struct {
typ int
s string
i int
isFunctionCall bool
callingArgs []functionCallArgument // needed for a function call, represents all argument nodes (INode supports nested function calls)
}
type functionCallArgument interface {
Evaluate(*ExecutionContext) (*Value, *Error)
}
// TODO: Add location tokens
type stringResolver struct {
locationToken *Token
val string
}
type intResolver struct {
locationToken *Token
val int
}
type floatResolver struct {
locationToken *Token
val float64
}
type boolResolver struct {
locationToken *Token
val bool
}
type variableResolver struct {
locationToken *Token
parts []*variablePart
}
type nodeFilteredVariable struct {
locationToken *Token
resolver IEvaluator
filterChain []*filterCall
}
type nodeVariable struct {
locationToken *Token
expr IEvaluator
}
type executionCtxEval struct{}
func (v *nodeFilteredVariable) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
value, err := v.Evaluate(ctx)
if err != nil {
return err
}
writer.WriteString(value.String())
return nil
}
func (vr *variableResolver) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
value, err := vr.Evaluate(ctx)
if err != nil {
return err
}
writer.WriteString(value.String())
return nil
}
func (s *stringResolver) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
value, err := s.Evaluate(ctx)
if err != nil {
return err
}
writer.WriteString(value.String())
return nil
}
func (i *intResolver) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
value, err := i.Evaluate(ctx)
if err != nil {
return err
}
writer.WriteString(value.String())
return nil
}
func (f *floatResolver) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
value, err := f.Evaluate(ctx)
if err != nil {
return err
}
writer.WriteString(value.String())
return nil
}
func (b *boolResolver) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
value, err := b.Evaluate(ctx)
if err != nil {
return err
}
writer.WriteString(value.String())
return nil
}
func (v *nodeFilteredVariable) GetPositionToken() *Token {
return v.locationToken
}
func (vr *variableResolver) GetPositionToken() *Token {
return vr.locationToken
}
func (s *stringResolver) GetPositionToken() *Token {
return s.locationToken
}
func (i *intResolver) GetPositionToken() *Token {
return i.locationToken
}
func (f *floatResolver) GetPositionToken() *Token {
return f.locationToken
}
func (b *boolResolver) GetPositionToken() *Token {
return b.locationToken
}
func (s *stringResolver) Evaluate(ctx *ExecutionContext) (*Value, *Error) {
return AsValue(s.val), nil
}
func (i *intResolver) Evaluate(ctx *ExecutionContext) (*Value, *Error) {
return AsValue(i.val), nil
}
func (f *floatResolver) Evaluate(ctx *ExecutionContext) (*Value, *Error) {
return AsValue(f.val), nil
}
func (b *boolResolver) Evaluate(ctx *ExecutionContext) (*Value, *Error) {
return AsValue(b.val), nil
}
func (s *stringResolver) FilterApplied(name string) bool {
return false
}
func (i *intResolver) FilterApplied(name string) bool {
return false
}
func (f *floatResolver) FilterApplied(name string) bool {
return false
}
func (b *boolResolver) FilterApplied(name string) bool {
return false
}
func (nv *nodeVariable) FilterApplied(name string) bool {
return nv.expr.FilterApplied(name)
}
func (nv *nodeVariable) Execute(ctx *ExecutionContext, writer TemplateWriter) *Error {
value, err := nv.expr.Evaluate(ctx)
if err != nil {
return err
}
if !nv.expr.FilterApplied("safe") && !value.safe && value.IsString() && ctx.Autoescape {
// apply escape filter
value, err = filters["escape"](value, nil)
if err != nil {
return err
}
}
writer.WriteString(value.String())
return nil
}
func (executionCtxEval) Evaluate(ctx *ExecutionContext) (*Value, *Error) {
return AsValue(ctx), nil
}
func (vr *variableResolver) FilterApplied(name string) bool {
return false
}
func (vr *variableResolver) String() string {
parts := make([]string, 0, len(vr.parts))
for _, p := range vr.parts {
switch p.typ {
case varTypeInt:
parts = append(parts, strconv.Itoa(p.i))
case varTypeIdent:
parts = append(parts, p.s)
default:
panic("unimplemented")
}
}
return strings.Join(parts, ".")
}
func (vr *variableResolver) resolve(ctx *ExecutionContext) (*Value, error) {
var current reflect.Value
var isSafe bool
for idx, part := range vr.parts {
if idx == 0 {
// We're looking up the first part of the variable.
// First we're having a look in our private
// context (e. g. information provided by tags, like the forloop)
val, inPrivate := ctx.Private[vr.parts[0].s]
if !inPrivate {
// Nothing found? Then have a final lookup in the public context
val = ctx.Public[vr.parts[0].s]
}
current = reflect.ValueOf(val) // Get the initial value
} else {
// Next parts, resolve it from current
// Before resolving the pointer, let's see if we have a method to call
// Problem with resolving the pointer is we're changing the receiver
isFunc := false
if part.typ == varTypeIdent {
funcValue := current.MethodByName(part.s)
if funcValue.IsValid() {
current = funcValue
isFunc = true
}
}
if !isFunc {
// If current a pointer, resolve it
if current.Kind() == reflect.Ptr {
current = current.Elem()
if !current.IsValid() {
// Value is not valid (anymore)
return AsValue(nil), nil
}
}
// Look up which part must be called now
switch part.typ {
case varTypeInt:
// Calling an index is only possible for:
// * slices/arrays/strings
switch current.Kind() {
case reflect.String, reflect.Array, reflect.Slice:
if part.i >= 0 && current.Len() > part.i {
current = current.Index(part.i)
} else {
// In Django, exceeding the length of a list is just empty.
return AsValue(nil), nil
}
default:
return nil, fmt.Errorf("Can't access an index on type %s (variable %s)",
current.Kind().String(), vr.String())
}
case varTypeIdent:
// debugging:
// fmt.Printf("now = %s (kind: %s)\n", part.s, current.Kind().String())
// Calling a field or key
switch current.Kind() {
case reflect.Struct:
current = current.FieldByName(part.s)
case reflect.Map:
current = current.MapIndex(reflect.ValueOf(part.s))
default:
return nil, fmt.Errorf("Can't access a field by name on type %s (variable %s)",
current.Kind().String(), vr.String())
}
default:
panic("unimplemented")
}
}
}
if !current.IsValid() {
// Value is not valid (anymore)
return AsValue(nil), nil
}
// If current is a reflect.ValueOf(pongo2.Value), then unpack it
// Happens in function calls (as a return value) or by injecting
// into the execution context (e.g. in a for-loop)
if current.Type() == typeOfValuePtr {
tmpValue := current.Interface().(*Value)
current = tmpValue.val
isSafe = tmpValue.safe
}
// Check whether this is an interface and resolve it where required
if current.Kind() == reflect.Interface {
current = reflect.ValueOf(current.Interface())
}
// Check if the part is a function call
if part.isFunctionCall || current.Kind() == reflect.Func {
// Check for callable
if current.Kind() != reflect.Func {
return nil, fmt.Errorf("'%s' is not a function (it is %s)", vr.String(), current.Kind().String())
}
// Check for correct function syntax and types
// func(*Value, ...) *Value
t := current.Type()
currArgs := part.callingArgs
// If an implicit ExecCtx is needed
if t.NumIn() > 0 && t.In(0) == typeOfExecCtxPtr {
currArgs = append([]functionCallArgument{executionCtxEval{}}, currArgs...)
}
// Input arguments
if len(currArgs) != t.NumIn() && !(len(currArgs) >= t.NumIn()-1 && t.IsVariadic()) {
return nil,
fmt.Errorf("Function input argument count (%d) of '%s' must be equal to the calling argument count (%d).",
t.NumIn(), vr.String(), len(currArgs))
}
// Output arguments
if t.NumOut() != 1 && t.NumOut() != 2 {
return nil, fmt.Errorf("'%s' must have exactly 1 or 2 output arguments, the second argument must be of type error", vr.String())
}
// Evaluate all parameters
var parameters []reflect.Value
numArgs := t.NumIn()
isVariadic := t.IsVariadic()
var fnArg reflect.Type
for idx, arg := range currArgs {
pv, err := arg.Evaluate(ctx)
if err != nil {
return nil, err
}
if isVariadic {
if idx >= t.NumIn()-1 {
fnArg = t.In(numArgs - 1).Elem()
} else {
fnArg = t.In(idx)
}
} else {
fnArg = t.In(idx)
}
if fnArg != typeOfValuePtr {
// Function's argument is not a *pongo2.Value, then we have to check whether input argument is of the same type as the function's argument
if !isVariadic {
if fnArg != reflect.TypeOf(pv.Interface()) && fnArg.Kind() != reflect.Interface {
return nil, fmt.Errorf("Function input argument %d of '%s' must be of type %s or *pongo2.Value (not %T).",
idx, vr.String(), fnArg.String(), pv.Interface())
}
// Function's argument has another type, using the interface-value
parameters = append(parameters, reflect.ValueOf(pv.Interface()))
} else {
if fnArg != reflect.TypeOf(pv.Interface()) && fnArg.Kind() != reflect.Interface {
return nil, fmt.Errorf("Function variadic input argument of '%s' must be of type %s or *pongo2.Value (not %T).",
vr.String(), fnArg.String(), pv.Interface())
}
// Function's argument has another type, using the interface-value
parameters = append(parameters, reflect.ValueOf(pv.Interface()))
}
} else {
// Function's argument is a *pongo2.Value
parameters = append(parameters, reflect.ValueOf(pv))
}
}
// Check if any of the values are invalid
for _, p := range parameters {
if p.Kind() == reflect.Invalid {
return nil, fmt.Errorf("Calling a function using an invalid parameter")
}
}
// Call it and get first return parameter back
values := current.Call(parameters)
rv := values[0]
if t.NumOut() == 2 {
e := values[1].Interface()
if e != nil {
err, ok := e.(error)
if !ok {
return nil, fmt.Errorf("The second return value is not an error")
}
if err != nil {
return nil, err
}
}
}
if rv.Type() != typeOfValuePtr {
current = reflect.ValueOf(rv.Interface())
} else {
// Return the function call value
current = rv.Interface().(*Value).val
isSafe = rv.Interface().(*Value).safe
}
}
if !current.IsValid() {
// Value is not valid (e. g. NIL value)
return AsValue(nil), nil
}
}
return &Value{val: current, safe: isSafe}, nil
}
func (vr *variableResolver) Evaluate(ctx *ExecutionContext) (*Value, *Error) {
value, err := vr.resolve(ctx)
if err != nil {
return AsValue(nil), ctx.Error(err.Error(), vr.locationToken)
}
return value, nil
}
func (v *nodeFilteredVariable) FilterApplied(name string) bool {
for _, filter := range v.filterChain {
if filter.name == name {
return true
}
}
return false
}
func (v *nodeFilteredVariable) Evaluate(ctx *ExecutionContext) (*Value, *Error) {
value, err := v.resolver.Evaluate(ctx)
if err != nil {
return nil, err
}
for _, filter := range v.filterChain {
value, err = filter.Execute(value, ctx)
if err != nil {
return nil, err
}
}
return value, nil
}
// IDENT | IDENT.(IDENT|NUMBER)...
func (p *Parser) parseVariableOrLiteral() (IEvaluator, *Error) {
t := p.Current()
if t == nil {
return nil, p.Error("Unexpected EOF, expected a number, string, keyword or identifier.", p.lastToken)
}
// Is first part a number or a string, there's nothing to resolve (because there's only to return the value then)
switch t.Typ {
case TokenNumber:
p.Consume()
// One exception to the rule that we don't have float64 literals is at the beginning
// of an expression (or a variable name). Since we know we started with an integer
// which can't obviously be a variable name, we can check whether the first number
// is followed by dot (and then a number again). If so we're converting it to a float64.
if p.Match(TokenSymbol, ".") != nil {
// float64
t2 := p.MatchType(TokenNumber)
if t2 == nil {
return nil, p.Error("Expected a number after the '.'.", nil)
}
f, err := strconv.ParseFloat(fmt.Sprintf("%s.%s", t.Val, t2.Val), 64)
if err != nil {
return nil, p.Error(err.Error(), t)
}
fr := &floatResolver{
locationToken: t,
val: f,
}
return fr, nil
}
i, err := strconv.Atoi(t.Val)
if err != nil {
return nil, p.Error(err.Error(), t)
}
nr := &intResolver{
locationToken: t,
val: i,
}
return nr, nil
case TokenString:
p.Consume()
sr := &stringResolver{
locationToken: t,
val: t.Val,
}
return sr, nil
case TokenKeyword:
p.Consume()
switch t.Val {
case "true":
br := &boolResolver{
locationToken: t,
val: true,
}
return br, nil
case "false":
br := &boolResolver{
locationToken: t,
val: false,
}
return br, nil
default:
return nil, p.Error("This keyword is not allowed here.", nil)
}
}
resolver := &variableResolver{
locationToken: t,
}
// First part of a variable MUST be an identifier
if t.Typ != TokenIdentifier {
return nil, p.Error("Expected either a number, string, keyword or identifier.", t)
}
resolver.parts = append(resolver.parts, &variablePart{
typ: varTypeIdent,
s: t.Val,
})
p.Consume() // we consumed the first identifier of the variable name
variableLoop:
for p.Remaining() > 0 {
t = p.Current()
if p.Match(TokenSymbol, ".") != nil {
// Next variable part (can be either NUMBER or IDENT)
t2 := p.Current()
if t2 != nil {
switch t2.Typ {
case TokenIdentifier:
resolver.parts = append(resolver.parts, &variablePart{
typ: varTypeIdent,
s: t2.Val,
})
p.Consume() // consume: IDENT
continue variableLoop
case TokenNumber:
i, err := strconv.Atoi(t2.Val)
if err != nil {
return nil, p.Error(err.Error(), t2)
}
resolver.parts = append(resolver.parts, &variablePart{
typ: varTypeInt,
i: i,
})
p.Consume() // consume: NUMBER
continue variableLoop
default:
return nil, p.Error("This token is not allowed within a variable name.", t2)
}
} else {
// EOF
return nil, p.Error("Unexpected EOF, expected either IDENTIFIER or NUMBER after DOT.",
p.lastToken)
}
} else if p.Match(TokenSymbol, "(") != nil {
// Function call
// FunctionName '(' Comma-separated list of expressions ')'
part := resolver.parts[len(resolver.parts)-1]
part.isFunctionCall = true
argumentLoop:
for {
if p.Remaining() == 0 {
return nil, p.Error("Unexpected EOF, expected function call argument list.", p.lastToken)
}
if p.Peek(TokenSymbol, ")") == nil {
// No closing bracket, so we're parsing an expression
exprArg, err := p.ParseExpression()
if err != nil {
return nil, err
}
part.callingArgs = append(part.callingArgs, exprArg)
if p.Match(TokenSymbol, ")") != nil {
// If there's a closing bracket after an expression, we will stop parsing the arguments
break argumentLoop
} else {
// If there's NO closing bracket, there MUST be an comma
if p.Match(TokenSymbol, ",") == nil {
return nil, p.Error("Missing comma or closing bracket after argument.", nil)
}
}
} else {
// We got a closing bracket, so stop parsing arguments
p.Consume()
break argumentLoop
}
}
// We're done parsing the function call, next variable part
continue variableLoop
}
// No dot or function call? Then we're done with the variable parsing
break
}
return resolver, nil
}
func (p *Parser) parseVariableOrLiteralWithFilter() (*nodeFilteredVariable, *Error) {
v := &nodeFilteredVariable{
locationToken: p.Current(),
}
// Parse the variable name
resolver, err := p.parseVariableOrLiteral()
if err != nil {
return nil, err
}
v.resolver = resolver
// Parse all the filters
filterLoop:
for p.Match(TokenSymbol, "|") != nil {
// Parse one single filter
filter, err := p.parseFilter()
if err != nil {
return nil, err
}
// Check sandbox filter restriction
if _, isBanned := p.template.set.bannedFilters[filter.name]; isBanned {
return nil, p.Error(fmt.Sprintf("Usage of filter '%s' is not allowed (sandbox restriction active).", filter.name), nil)
}
v.filterChain = append(v.filterChain, filter)
continue filterLoop
}
return v, nil
}
func (p *Parser) parseVariableElement() (INode, *Error) {
node := &nodeVariable{
locationToken: p.Current(),
}
p.Consume() // consume '{{'
expr, err := p.ParseExpression()
if err != nil {
return nil, err
}
node.expr = expr
if p.Match(TokenSymbol, "}}") == nil {
return nil, p.Error("'}}' expected", nil)
}
return node, nil
}

View file

@ -1,187 +0,0 @@
Copyright © 2014, Roger Peppe, Canonical Inc.
This software is licensed under the LGPLv3, included below.
As a special exception to the GNU Lesser General Public License version 3
("LGPL3"), the copyright holders of this Library give you permission to
convey to a third party a Combined Work that links statically or dynamically
to this Library without providing any Minimal Corresponding Source or
Minimal Application Code as set out in 4d or providing the installation
information set out in section 4e, provided that you comply with the other
provisions of LGPL3 and provided that you meet, for the Application the
terms and conditions of the license(s) which apply to the Application.
Except as stated in this special exception, the provisions of LGPL3 will
continue to comply in full to this Library. If you modify this Library, you
may apply this exception to your version of this Library, but you are not
obliged to do so. If you do not wish to do so, delete this exception
statement from your version. This exception does not (and cannot) modify any
license terms which apply to the Application, with which you must still
comply.
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View file

@ -1,97 +0,0 @@
package bakery
import (
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
)
// Bakery is a convenience type that contains both an Oven
// and a Checker.
type Bakery struct {
Oven *Oven
Checker *Checker
}
// BakeryParams holds a selection of parameters for the Oven
// and the Checker created by New.
//
// For more fine-grained control of parameters, create the
// Oven or Checker directly.
//
// The zero value is OK to use, but won't allow any authentication
// or third party caveats to be added.
type BakeryParams struct {
// Logger is used to send log messages. If it is nil,
// nothing will be logged.
Logger Logger
// Checker holds the checker used to check first party caveats.
// If this is nil, New will use checkers.New(nil).
Checker FirstPartyCaveatChecker
// RootKeyStore holds the root key store to use. If you need to
// use a different root key store for different operations,
// you'll need to pass a RootKeyStoreForOps value to NewOven
// directly.
//
// If this is nil, New will use NewMemRootKeyStore().
// Note that that is almost certain insufficient for production services
// that are spread across multiple instances or that need
// to persist keys across restarts.
RootKeyStore RootKeyStore
// Locator is used to find out information on third parties when
// adding third party caveats. If this is nil, no non-local third
// party caveats can be added.
Locator ThirdPartyLocator
// Key holds the private key of the oven. If this is nil,
// no third party caveats may be added.
Key *KeyPair
// OpsAuthorizer is used to check whether operations are authorized
// by some other already-authorized operation. If it is nil,
// NewChecker will assume no operation is authorized by any
// operation except itself.
OpsAuthorizer OpsAuthorizer
// Location holds the location to use when creating new macaroons.
Location string
// LegacyMacaroonOp holds the operation to associate with old
// macaroons that don't have associated operations.
// If this is empty, legacy macaroons will not be associated
// with any operations.
LegacyMacaroonOp Op
}
// New returns a new Bakery instance which combines an Oven with a
// Checker for the convenience of callers that wish to use both
// together.
func New(p BakeryParams) *Bakery {
if p.Checker == nil {
p.Checker = checkers.New(nil)
}
ovenParams := OvenParams{
Key: p.Key,
Namespace: p.Checker.Namespace(),
Location: p.Location,
Locator: p.Locator,
LegacyMacaroonOp: p.LegacyMacaroonOp,
}
if p.RootKeyStore != nil {
ovenParams.RootKeyStoreForOps = func(ops []Op) RootKeyStore {
return p.RootKeyStore
}
}
oven := NewOven(ovenParams)
checker := NewChecker(CheckerParams{
Checker: p.Checker,
MacaroonVerifier: oven,
OpsAuthorizer: p.OpsAuthorizer,
})
return &Bakery{
Oven: oven,
Checker: checker,
}
}

View file

@ -1,503 +0,0 @@
package bakery
import (
"context"
"sort"
"sync"
"time"
"gopkg.in/errgo.v1"
"gopkg.in/macaroon.v2"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
)
// Op holds an entity and action to be authorized on that entity.
type Op struct {
// Entity holds the name of the entity to be authorized.
// Entity names should not contain spaces and should
// not start with the prefix "login" or "multi-" (conventionally,
// entity names will be prefixed with the entity type followed
// by a hyphen.
Entity string
// Action holds the action to perform on the entity, such as "read"
// or "delete". It is up to the service using a checker to define
// a set of operations and keep them consistent over time.
Action string
}
// NoOp holds the empty operation, signifying no authorized
// operation. This is always considered to be authorized.
// See OpsAuthorizer for one place that it's used.
var NoOp = Op{}
// CheckerParams holds parameters for NewChecker.
type CheckerParams struct {
// Checker is used to check first party caveats when authorizing.
// If this is nil NewChecker will use checkers.New(nil).
Checker FirstPartyCaveatChecker
// OpsAuthorizer is used to check whether operations are authorized
// by some other already-authorized operation. If it is nil,
// NewChecker will assume no operation is authorized by any
// operation except itself.
OpsAuthorizer OpsAuthorizer
// MacaroonVerifier is used to verify macaroons.
MacaroonVerifier MacaroonVerifier
// Logger is used to log checker operations. If it is nil,
// DefaultLogger("bakery") will be used.
Logger Logger
}
// OpsAuthorizer is used to check whether an operation authorizes some other
// operation. For example, a macaroon with an operation allowing general access to a service
// might also grant access to a more specific operation.
type OpsAuthorizer interface {
// AuthorizeOp reports which elements of queryOps are authorized by
// authorizedOp. On return, each element of the slice should represent
// whether the respective element in queryOps has been authorized.
// An empty returned slice indicates that no operations are authorized.
// AuthorizeOps may also return third party caveats that apply to
// the authorized operations. Access will only be authorized when
// those caveats are discharged by the client.
//
// When not all operations can be authorized with the macaroons
// supplied to Checker.Auth, the checker will call AuthorizeOps
// with NoOp, because some operations might be authorized
// regardless of authority. NoOp will always be the last
// operation queried within any given Allow call.
//
// AuthorizeOps should only return an error if authorization cannot be checked
// (for example because of a database access failure), not because
// authorization was denied.
AuthorizeOps(ctx context.Context, authorizedOp Op, queryOps []Op) ([]bool, []checkers.Caveat, error)
}
// AuthInfo information about an authorization decision.
type AuthInfo struct {
// Macaroons holds all the macaroons that were
// passed to Auth.
Macaroons []macaroon.Slice
// Used records which macaroons were used in the
// authorization decision. It holds one element for
// each element of Macaroons. Macaroons that
// were invalid or unnecessary will have a false entry.
Used []bool
// OpIndexes holds the index of each macaroon
// that was used to authorize an operation.
OpIndexes map[Op]int
}
// Conditions returns the first party caveat caveat conditions hat apply to
// the given AuthInfo. This can be used to apply appropriate caveats
// to capability macaroons granted via a Checker.Allow call.
func (a *AuthInfo) Conditions() []string {
var squasher caveatSquasher
for i, ms := range a.Macaroons {
if !a.Used[i] {
continue
}
for _, m := range ms {
for _, cav := range m.Caveats() {
if len(cav.VerificationId) > 0 {
continue
}
squasher.add(string(cav.Id))
}
}
}
return squasher.final()
}
// Checker wraps a FirstPartyCaveatChecker and adds authentication and authorization checks.
//
// It uses macaroons as authorization tokens but it is not itself responsible for
// creating the macaroons - see the Oven type (TODO) for one way of doing that.
type Checker struct {
FirstPartyCaveatChecker
p CheckerParams
}
// NewChecker returns a new Checker using the given parameters.
func NewChecker(p CheckerParams) *Checker {
if p.Checker == nil {
p.Checker = checkers.New(nil)
}
if p.Logger == nil {
p.Logger = DefaultLogger("bakery")
}
return &Checker{
FirstPartyCaveatChecker: p.Checker,
p: p,
}
}
// Auth makes a new AuthChecker instance using the
// given macaroons to inform authorization decisions.
func (c *Checker) Auth(mss ...macaroon.Slice) *AuthChecker {
return &AuthChecker{
Checker: c,
macaroons: mss,
}
}
// AuthChecker authorizes operations with respect to a user's request.
type AuthChecker struct {
// Checker is used to check first party caveats.
*Checker
macaroons []macaroon.Slice
// conditions holds the first party caveat conditions
// that apply to each of the above macaroons.
conditions [][]string
initOnce sync.Once
initError error
initErrors []error
// authIndexes holds for each potentially authorized operation
// the indexes of the macaroons that authorize it.
authIndexes map[Op][]int
}
func (a *AuthChecker) init(ctx context.Context) error {
a.initOnce.Do(func() {
a.initError = a.initOnceFunc(ctx)
})
return a.initError
}
func (a *AuthChecker) initOnceFunc(ctx context.Context) error {
a.authIndexes = make(map[Op][]int)
a.conditions = make([][]string, len(a.macaroons))
for i, ms := range a.macaroons {
ops, conditions, err := a.p.MacaroonVerifier.VerifyMacaroon(ctx, ms)
if err != nil {
if !isVerificationError(err) {
return errgo.Notef(err, "cannot retrieve macaroon")
}
a.initErrors = append(a.initErrors, errgo.Mask(err))
continue
}
a.p.Logger.Debugf(ctx, "macaroon %d has valid sig; ops %q, conditions %q", i, ops, conditions)
// It's a valid macaroon (in principle - we haven't checked first party caveats).
a.conditions[i] = conditions
for _, op := range ops {
a.authIndexes[op] = append(a.authIndexes[op], i)
}
}
return nil
}
// Allowed returns an AuthInfo that provides information on all
// operations directly authorized by the macaroons provided
// to Checker.Auth. Note that this does not include operations that would be indirectly
// allowed via the OpAuthorizer.
//
// Allowed returns an error only when there is an underlying storage failure,
// not when operations are not authorized.
func (a *AuthChecker) Allowed(ctx context.Context) (*AuthInfo, error) {
actx, err := a.newAllowContext(ctx, nil)
if err != nil {
return nil, errgo.Mask(err)
}
for op, mindexes := range a.authIndexes {
for _, mindex := range mindexes {
if actx.status[mindex]&statusOK != 0 {
actx.status[mindex] |= statusUsed
actx.opIndexes[op] = mindex
break
}
}
}
return actx.newAuthInfo(), nil
}
func (a *allowContext) newAuthInfo() *AuthInfo {
info := &AuthInfo{
Macaroons: a.checker.macaroons,
Used: make([]bool, len(a.checker.macaroons)),
OpIndexes: a.opIndexes,
}
for i, status := range a.status {
if status&statusUsed != 0 {
info.Used[i] = true
}
}
return info
}
// allowContext holds temporary state used by AuthChecker.allowAny.
type allowContext struct {
checker *AuthChecker
// status holds used and authorized status of all the
// request macaroons.
status []macaroonStatus
// opIndex holds an entry for each authorized operation
// that refers to the macaroon that authorized that operation.
opIndexes map[Op]int
// authed holds which of the requested operations have
// been authorized so far.
authed []bool
// need holds all of the requested operations that
// are remaining to be authorized. needIndex holds the
// index of each of these operations in the original operations slice
need []Op
needIndex []int
// errors holds any errors encountered during authorization.
errors []error
}
type macaroonStatus uint8
const (
statusOK = 1 << iota
statusUsed
)
func (a *AuthChecker) newAllowContext(ctx context.Context, ops []Op) (*allowContext, error) {
actx := &allowContext{
checker: a,
status: make([]macaroonStatus, len(a.macaroons)),
authed: make([]bool, len(ops)),
need: append([]Op(nil), ops...),
needIndex: make([]int, len(ops)),
opIndexes: make(map[Op]int),
}
for i := range actx.needIndex {
actx.needIndex[i] = i
}
if err := a.init(ctx); err != nil {
return actx, errgo.Mask(err)
}
// Check all the macaroons with respect to the current context.
// Technically this is more than we need to do, because some
// of the macaroons might not authorize the specific operations
// we're interested in, but that's an optimisation that could happen
// later if performance becomes an issue with respect to that.
outer:
for i, ms := range a.macaroons {
ctx := checkers.ContextWithMacaroons(ctx, a.Namespace(), ms)
for _, cond := range a.conditions[i] {
if err := a.CheckFirstPartyCaveat(ctx, cond); err != nil {
actx.addError(err)
continue outer
}
}
actx.status[i] = statusOK
}
return actx, nil
}
// Macaroons returns the macaroons that were passed
// to Checker.Auth when creating the AuthChecker.
func (a *AuthChecker) Macaroons() []macaroon.Slice {
return a.macaroons
}
// Allow checks that the authorizer's request is authorized to
// perform all the given operations.
//
// If all the operations are allowed, an AuthInfo is returned holding
// details of the decision.
//
// If an operation was not allowed, an error will be returned which may
// be *DischargeRequiredError holding the operations that remain to
// be authorized in order to allow authorization to
// proceed.
func (a *AuthChecker) Allow(ctx context.Context, ops ...Op) (*AuthInfo, error) {
actx, err := a.newAllowContext(ctx, ops)
if err != nil {
return nil, errgo.Mask(err)
}
actx.checkDirect(ctx)
if len(actx.need) == 0 {
return actx.newAuthInfo(), nil
}
caveats, err := actx.checkIndirect(ctx)
if err != nil {
return nil, errgo.Mask(err)
}
if len(actx.need) == 0 && len(caveats) == 0 {
// No more ops need to be authenticated and no caveats to be discharged.
return actx.newAuthInfo(), nil
}
a.p.Logger.Debugf(ctx, "operations still needed after auth check: %#v", actx.need)
if len(caveats) == 0 || len(actx.need) > 0 {
allErrors := make([]error, 0, len(a.initErrors)+len(actx.errors))
allErrors = append(allErrors, a.initErrors...)
allErrors = append(allErrors, actx.errors...)
var err error
if len(allErrors) > 0 {
// TODO return all errors?
a.p.Logger.Infof(ctx, "all auth errors: %q", allErrors)
err = allErrors[0]
}
return nil, errgo.WithCausef(err, ErrPermissionDenied, "")
}
return nil, &DischargeRequiredError{
Message: "some operations have extra caveats",
Ops: ops,
Caveats: caveats,
}
}
// checkDirect checks which operations are directly authorized by
// the macaroon operations.
func (a *allowContext) checkDirect(ctx context.Context) {
defer a.updateNeed()
for i, op := range a.need {
if op == NoOp {
// NoOp is always authorized.
a.authed[a.needIndex[i]] = true
continue
}
for _, mindex := range a.checker.authIndexes[op] {
if a.status[mindex]&statusOK != 0 {
a.authed[a.needIndex[i]] = true
a.status[mindex] |= statusUsed
a.opIndexes[op] = mindex
break
}
}
}
}
// checkIndirect checks to see if any of the remaining operations are authorized
// indirectly with the already-authorized operations.
func (a *allowContext) checkIndirect(ctx context.Context) ([]checkers.Caveat, error) {
if a.checker.p.OpsAuthorizer == nil {
return nil, nil
}
var allCaveats []checkers.Caveat
for op, mindexes := range a.checker.authIndexes {
if len(a.need) == 0 {
break
}
for _, mindex := range mindexes {
if a.status[mindex]&statusOK == 0 {
continue
}
ctx := checkers.ContextWithMacaroons(ctx, a.checker.Namespace(), a.checker.macaroons[mindex])
authedOK, caveats, err := a.checker.p.OpsAuthorizer.AuthorizeOps(ctx, op, a.need)
if err != nil {
return nil, errgo.Mask(err)
}
// TODO we could perhaps combine identical third party caveats here.
allCaveats = append(allCaveats, caveats...)
for i, ok := range authedOK {
if !ok {
continue
}
// Operation is authorized. Mark the appropriate macaroon as used,
// and remove the operation from the needed list so that we don't
// bother AuthorizeOps with it again.
a.status[mindex] |= statusUsed
a.authed[a.needIndex[i]] = true
a.opIndexes[a.need[i]] = mindex
}
}
a.updateNeed()
}
if len(a.need) == 0 {
return allCaveats, nil
}
// We've still got at least one operation unauthorized.
// Try to see if it can be authorized with no operation at all.
authedOK, caveats, err := a.checker.p.OpsAuthorizer.AuthorizeOps(ctx, NoOp, a.need)
if err != nil {
return nil, errgo.Mask(err)
}
allCaveats = append(allCaveats, caveats...)
for i, ok := range authedOK {
if ok {
a.authed[a.needIndex[i]] = true
}
}
a.updateNeed()
return allCaveats, nil
}
// updateNeed removes all authorized operations from a.need
// and updates a.needIndex appropriately too.
func (a *allowContext) updateNeed() {
j := 0
for i, opIndex := range a.needIndex {
if a.authed[opIndex] {
continue
}
if i != j {
a.need[j], a.needIndex[j] = a.need[i], a.needIndex[i]
}
j++
}
a.need, a.needIndex = a.need[0:j], a.needIndex[0:j]
}
func (a *allowContext) addError(err error) {
a.errors = append(a.errors, err)
}
// caveatSquasher rationalizes first party caveats created for a capability
// by:
// - including only the earliest time-before caveat.
// - removing duplicates.
type caveatSquasher struct {
expiry time.Time
conds []string
}
func (c *caveatSquasher) add(cond string) {
if c.add0(cond) {
c.conds = append(c.conds, cond)
}
}
func (c *caveatSquasher) add0(cond string) bool {
cond, args, err := checkers.ParseCaveat(cond)
if err != nil {
// Be safe - if we can't parse the caveat, just leave it there.
return true
}
if cond != checkers.CondTimeBefore {
return true
}
et, err := time.Parse(time.RFC3339Nano, args)
if err != nil || et.IsZero() {
// Again, if it doesn't seem valid, leave it alone.
return true
}
if c.expiry.IsZero() || et.Before(c.expiry) {
c.expiry = et
}
return false
}
func (c *caveatSquasher) final() []string {
if !c.expiry.IsZero() {
c.conds = append(c.conds, checkers.TimeBeforeCaveat(c.expiry).Condition)
}
if len(c.conds) == 0 {
return nil
}
// Make deterministic and eliminate duplicates.
sort.Strings(c.conds)
prev := c.conds[0]
j := 1
for _, cond := range c.conds[1:] {
if cond != prev {
c.conds[j] = cond
prev = cond
j++
}
}
c.conds = c.conds[:j]
return c.conds
}

View file

@ -1,246 +0,0 @@
// The checkers package provides some standard first-party
// caveat checkers and some primitives for combining them.
package checkers
import (
"context"
"fmt"
"sort"
"strings"
"gopkg.in/errgo.v1"
)
// StdNamespace holds the URI of the standard checkers schema.
const StdNamespace = "std"
// Constants for all the standard caveat conditions.
// First and third party caveat conditions are both defined here,
// even though notionally they exist in separate name spaces.
const (
CondDeclared = "declared"
CondTimeBefore = "time-before"
CondError = "error"
)
const (
CondNeedDeclared = "need-declared"
)
// Func is the type of a function used by Checker to check a caveat. The
// cond parameter will hold the caveat condition including any namespace
// prefix; the arg parameter will hold any additional caveat argument
// text.
type Func func(ctx context.Context, cond, arg string) error
// CheckerInfo holds information on a registered checker.
type CheckerInfo struct {
// Check holds the actual checker function.
Check Func
// Prefix holds the prefix for the checker condition.
Prefix string
// Name holds the name of the checker condition.
Name string
// Namespace holds the namespace URI for the checker's
// schema.
Namespace string
}
var allCheckers = map[string]Func{
CondTimeBefore: checkTimeBefore,
CondDeclared: checkDeclared,
CondError: checkError,
}
// NewEmpty returns a checker using the given namespace
// that has no registered checkers.
// If ns is nil, a new one will be created.
func NewEmpty(ns *Namespace) *Checker {
if ns == nil {
ns = NewNamespace(nil)
}
return &Checker{
namespace: ns,
checkers: make(map[string]CheckerInfo),
}
}
// RegisterStd registers all the standard checkers in the given checker.
// If not present already, the standard checkers schema (StdNamespace) is
// added to the checker's namespace with an empty prefix.
func RegisterStd(c *Checker) {
c.namespace.Register(StdNamespace, "")
for cond, check := range allCheckers {
c.Register(cond, StdNamespace, check)
}
}
// New returns a checker with all the standard caveats checkers registered.
// If ns is nil, a new one will be created.
// The standard namespace is also added to ns if not present.
func New(ns *Namespace) *Checker {
c := NewEmpty(ns)
RegisterStd(c)
return c
}
// Checker holds a set of checkers for first party caveats.
// It implements bakery.CheckFirstParty caveat.
type Checker struct {
namespace *Namespace
checkers map[string]CheckerInfo
}
// Register registers the given condition in the given namespace URI
// to be checked with the given check function.
// It will panic if the namespace is not registered or
// if the condition has already been registered.
func (c *Checker) Register(cond, uri string, check Func) {
if check == nil {
panic(fmt.Errorf("nil check function registered for namespace %q when registering condition %q", uri, cond))
}
prefix, ok := c.namespace.Resolve(uri)
if !ok {
panic(fmt.Errorf("no prefix registered for namespace %q when registering condition %q", uri, cond))
}
if prefix == "" && strings.Contains(cond, ":") {
panic(fmt.Errorf("caveat condition %q in namespace %q contains a colon but its prefix is empty", cond, uri))
}
fullCond := ConditionWithPrefix(prefix, cond)
if info, ok := c.checkers[fullCond]; ok {
panic(fmt.Errorf("checker for %q (namespace %q) already registered in namespace %q", fullCond, uri, info.Namespace))
}
c.checkers[fullCond] = CheckerInfo{
Check: check,
Namespace: uri,
Name: cond,
Prefix: prefix,
}
}
// Info returns information on all the registered checkers, sorted by namespace
// and then name.
func (c *Checker) Info() []CheckerInfo {
checkers := make([]CheckerInfo, 0, len(c.checkers))
for _, c := range c.checkers {
checkers = append(checkers, c)
}
sort.Sort(checkerInfoByName(checkers))
return checkers
}
// Namespace returns the namespace associated with the
// checker. It implements bakery.FirstPartyCaveatChecker.Namespace.
func (c *Checker) Namespace() *Namespace {
return c.namespace
}
// CheckFirstPartyCaveat implements bakery.FirstPartyCaveatChecker
// by checking the caveat against all registered caveats conditions.
func (c *Checker) CheckFirstPartyCaveat(ctx context.Context, cav string) error {
cond, arg, err := ParseCaveat(cav)
if err != nil {
// If we can't parse it, perhaps it's in some other format,
// return a not-recognised error.
return errgo.WithCausef(err, ErrCaveatNotRecognized, "cannot parse caveat %q", cav)
}
cf, ok := c.checkers[cond]
if !ok {
return errgo.NoteMask(ErrCaveatNotRecognized, fmt.Sprintf("caveat %q not satisfied", cav), errgo.Any)
}
if err := cf.Check(ctx, cond, arg); err != nil {
return errgo.NoteMask(err, fmt.Sprintf("caveat %q not satisfied", cav), errgo.Any)
}
return nil
}
var errBadCaveat = errgo.New("bad caveat")
func checkError(ctx context.Context, _, arg string) error {
return errBadCaveat
}
// ErrCaveatNotRecognized is the cause of errors returned
// from caveat checkers when the caveat was not
// recognized.
var ErrCaveatNotRecognized = errgo.New("caveat not recognized")
// Caveat represents a condition that must be true for a check to
// complete successfully. If Location is non-empty, the caveat must be
// discharged by a third party at the given location.
// The Namespace field holds the namespace URI of the
// condition - if it is non-empty, it will be converted to
// a namespace prefix before adding to the macaroon.
type Caveat struct {
Condition string
Namespace string
Location string
}
// Condition builds a caveat condition from the given name and argument.
func Condition(name, arg string) string {
if arg == "" {
return name
}
return name + " " + arg
}
func firstParty(name, arg string) Caveat {
return Caveat{
Condition: Condition(name, arg),
Namespace: StdNamespace,
}
}
// ParseCaveat parses a caveat into an identifier, identifying the
// checker that should be used, and the argument to the checker (the
// rest of the string).
//
// The identifier is taken from all the characters before the first
// space character.
func ParseCaveat(cav string) (cond, arg string, err error) {
if cav == "" {
return "", "", fmt.Errorf("empty caveat")
}
i := strings.IndexByte(cav, ' ')
if i < 0 {
return cav, "", nil
}
if i == 0 {
return "", "", fmt.Errorf("caveat starts with space character")
}
return cav[0:i], cav[i+1:], nil
}
// ErrorCaveatf returns a caveat that will never be satisfied, holding
// the given fmt.Sprintf formatted text as the text of the caveat.
//
// This should only be used for highly unusual conditions that are never
// expected to happen in practice, such as a malformed key that is
// conventionally passed as a constant. It's not a panic but you should
// only use it in cases where a panic might possibly be appropriate.
//
// This mechanism means that caveats can be created without error
// checking and a later systematic check at a higher level (in the
// bakery package) can produce an error instead.
func ErrorCaveatf(f string, a ...interface{}) Caveat {
return firstParty(CondError, fmt.Sprintf(f, a...))
}
type checkerInfoByName []CheckerInfo
func (c checkerInfoByName) Less(i, j int) bool {
info0, info1 := &c[i], &c[j]
if info0.Namespace != info1.Namespace {
return info0.Namespace < info1.Namespace
}
return info0.Name < info1.Name
}
func (c checkerInfoByName) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}
func (c checkerInfoByName) Len() int {
return len(c)
}

View file

@ -1,137 +0,0 @@
package checkers
import (
"context"
"strings"
"gopkg.in/errgo.v1"
"gopkg.in/macaroon.v2"
)
type macaroonsKey struct{}
type macaroonsValue struct {
ns *Namespace
ms macaroon.Slice
}
// ContextWithMacaroons returns the given context associated with a
// macaroon slice and the name space to use to interpret caveats in
// the macaroons.
func ContextWithMacaroons(ctx context.Context, ns *Namespace, ms macaroon.Slice) context.Context {
return context.WithValue(ctx, macaroonsKey{}, macaroonsValue{
ns: ns,
ms: ms,
})
}
// MacaroonsFromContext returns the namespace and macaroons associated
// with the context by ContextWithMacaroons. This can be used to
// implement "structural" first-party caveats that are predicated on
// the macaroons being validated.
func MacaroonsFromContext(ctx context.Context) (*Namespace, macaroon.Slice) {
v, _ := ctx.Value(macaroonsKey{}).(macaroonsValue)
return v.ns, v.ms
}
// DeclaredCaveat returns a "declared" caveat asserting that the given key is
// set to the given value. If a macaroon has exactly one first party
// caveat asserting the value of a particular key, then InferDeclared
// will be able to infer the value, and then DeclaredChecker will allow
// the declared value if it has the value specified here.
//
// If the key is empty or contains a space, DeclaredCaveat
// will return an error caveat.
func DeclaredCaveat(key string, value string) Caveat {
if strings.Contains(key, " ") || key == "" {
return ErrorCaveatf("invalid caveat 'declared' key %q", key)
}
return firstParty(CondDeclared, key+" "+value)
}
// NeedDeclaredCaveat returns a third party caveat that
// wraps the provided third party caveat and requires
// that the third party must add "declared" caveats for
// all the named keys.
// TODO(rog) namespaces in third party caveats?
func NeedDeclaredCaveat(cav Caveat, keys ...string) Caveat {
if cav.Location == "" {
return ErrorCaveatf("need-declared caveat is not third-party")
}
return Caveat{
Location: cav.Location,
Condition: CondNeedDeclared + " " + strings.Join(keys, ",") + " " + cav.Condition,
}
}
func checkDeclared(ctx context.Context, _, arg string) error {
parts := strings.SplitN(arg, " ", 2)
if len(parts) != 2 {
return errgo.Newf("declared caveat has no value")
}
ns, ms := MacaroonsFromContext(ctx)
attrs := InferDeclared(ns, ms)
val, ok := attrs[parts[0]]
if !ok {
return errgo.Newf("got %s=null, expected %q", parts[0], parts[1])
}
if val != parts[1] {
return errgo.Newf("got %s=%q, expected %q", parts[0], val, parts[1])
}
return nil
}
// InferDeclared retrieves any declared information from
// the given macaroons and returns it as a key-value map.
//
// Information is declared with a first party caveat as created
// by DeclaredCaveat.
//
// If there are two caveats that declare the same key with
// different values, the information is omitted from the map.
// When the caveats are later checked, this will cause the
// check to fail.
func InferDeclared(ns *Namespace, ms macaroon.Slice) map[string]string {
var conditions []string
for _, m := range ms {
for _, cav := range m.Caveats() {
if cav.Location == "" {
conditions = append(conditions, string(cav.Id))
}
}
}
return InferDeclaredFromConditions(ns, conditions)
}
// InferDeclaredFromConditions is like InferDeclared except that
// it is passed a set of first party caveat conditions rather than a set of macaroons.
func InferDeclaredFromConditions(ns *Namespace, conds []string) map[string]string {
var conflicts []string
// If we can't resolve that standard namespace, then we'll look for
// just bare "declared" caveats which will work OK for legacy
// macaroons with no namespace.
prefix, _ := ns.Resolve(StdNamespace)
declaredCond := prefix + CondDeclared
info := make(map[string]string)
for _, cond := range conds {
name, rest, _ := ParseCaveat(cond)
if name != declaredCond {
continue
}
parts := strings.SplitN(rest, " ", 2)
if len(parts) != 2 {
continue
}
key, val := parts[0], parts[1]
if oldVal, ok := info[key]; ok && oldVal != val {
conflicts = append(conflicts, key)
continue
}
info[key] = val
}
for _, key := range conflicts {
delete(info, key)
}
return info
}

View file

@ -1,214 +0,0 @@
package checkers
import (
"sort"
"strings"
"unicode"
"unicode/utf8"
"gopkg.in/errgo.v1"
)
// Namespace holds maps from schema URIs to the
// prefixes that are used to encode them in first party
// caveats. Several different URIs may map to the same
// prefix - this is usual when several different backwardly
// compatible schema versions are registered.
type Namespace struct {
uriToPrefix map[string]string
}
// Equal reports whether ns2 encodes the same namespace
// as the receiver.
func (ns1 *Namespace) Equal(ns2 *Namespace) bool {
if ns1 == ns2 || ns1 == nil || ns2 == nil {
return ns1 == ns2
}
if len(ns1.uriToPrefix) != len(ns2.uriToPrefix) {
return false
}
for k, v := range ns1.uriToPrefix {
if ns2.uriToPrefix[k] != v {
return false
}
}
return true
}
// NewNamespace returns a new namespace with the
// given initial contents. It will panic if any of the
// URI keys or their associated prefix are invalid
// (see IsValidSchemaURI and IsValidPrefix).
func NewNamespace(uriToPrefix map[string]string) *Namespace {
ns := &Namespace{
uriToPrefix: make(map[string]string),
}
for uri, prefix := range uriToPrefix {
ns.Register(uri, prefix)
}
return ns
}
// String returns the namespace representation as returned by
// ns.MarshalText.
func (ns *Namespace) String() string {
data, _ := ns.MarshalText()
return string(data)
}
// MarshalText implements encoding.TextMarshaler by
// returning all the elements in the namespace sorted by
// URI, joined to the associated prefix with a colon and
// separated with spaces.
func (ns *Namespace) MarshalText() ([]byte, error) {
if ns == nil || len(ns.uriToPrefix) == 0 {
return nil, nil
}
uris := make([]string, 0, len(ns.uriToPrefix))
dataLen := 0
for uri, prefix := range ns.uriToPrefix {
uris = append(uris, uri)
dataLen += len(uri) + 1 + len(prefix) + 1
}
sort.Strings(uris)
data := make([]byte, 0, dataLen)
for i, uri := range uris {
if i > 0 {
data = append(data, ' ')
}
data = append(data, uri...)
data = append(data, ':')
data = append(data, ns.uriToPrefix[uri]...)
}
return data, nil
}
func (ns *Namespace) UnmarshalText(data []byte) error {
uriToPrefix := make(map[string]string)
elems := strings.Fields(string(data))
for _, elem := range elems {
i := strings.LastIndex(elem, ":")
if i == -1 {
return errgo.Newf("no colon in namespace field %q", elem)
}
uri, prefix := elem[0:i], elem[i+1:]
if !IsValidSchemaURI(uri) {
// Currently this can't happen because the only invalid URIs
// are those which contain a space
return errgo.Newf("invalid URI %q in namespace field %q", uri, elem)
}
if !IsValidPrefix(prefix) {
return errgo.Newf("invalid prefix %q in namespace field %q", prefix, elem)
}
if _, ok := uriToPrefix[uri]; ok {
return errgo.Newf("duplicate URI %q in namespace %q", uri, data)
}
uriToPrefix[uri] = prefix
}
ns.uriToPrefix = uriToPrefix
return nil
}
// EnsureResolved tries to resolve the given schema URI to a prefix and
// returns the prefix and whether the resolution was successful. If the
// URI hasn't been registered but a compatible version has, the
// given URI is registered with the same prefix.
func (ns *Namespace) EnsureResolved(uri string) (string, bool) {
// TODO(rog) compatibility
return ns.Resolve(uri)
}
// Resolve resolves the given schema URI to its registered prefix and
// returns the prefix and whether the resolution was successful.
//
// If ns is nil, it is treated as if it were empty.
//
// Resolve does not mutate ns and may be called concurrently
// with other non-mutating Namespace methods.
func (ns *Namespace) Resolve(uri string) (string, bool) {
if ns == nil {
return "", false
}
prefix, ok := ns.uriToPrefix[uri]
return prefix, ok
}
// ResolveCaveat resolves the given caveat by using
// Resolve to map from its schema namespace to the appropriate prefix using
// Resolve. If there is no registered prefix for the namespace,
// it returns an error caveat.
//
// If ns.Namespace is empty or ns.Location is non-empty, it returns cav unchanged.
//
// If ns is nil, it is treated as if it were empty.
//
// ResolveCaveat does not mutate ns and may be called concurrently
// with other non-mutating Namespace methods.
func (ns *Namespace) ResolveCaveat(cav Caveat) Caveat {
// TODO(rog) If a namespace isn't registered, try to resolve it by
// resolving it to the latest compatible version that is
// registered.
if cav.Namespace == "" || cav.Location != "" {
return cav
}
prefix, ok := ns.Resolve(cav.Namespace)
if !ok {
errCav := ErrorCaveatf("caveat %q in unregistered namespace %q", cav.Condition, cav.Namespace)
if errCav.Namespace != cav.Namespace {
prefix, _ = ns.Resolve(errCav.Namespace)
}
cav = errCav
}
if prefix != "" {
cav.Condition = ConditionWithPrefix(prefix, cav.Condition)
}
cav.Namespace = ""
return cav
}
// ConditionWithPrefix returns the given string prefixed by the
// given prefix. If the prefix is non-empty, a colon
// is used to separate them.
func ConditionWithPrefix(prefix, condition string) string {
if prefix == "" {
return condition
}
return prefix + ":" + condition
}
// Register registers the given URI and associates it
// with the given prefix. If the URI has already been registered,
// this is a no-op.
func (ns *Namespace) Register(uri, prefix string) {
if !IsValidSchemaURI(uri) {
panic(errgo.Newf("cannot register invalid URI %q (prefix %q)", uri, prefix))
}
if !IsValidPrefix(prefix) {
panic(errgo.Newf("cannot register invalid prefix %q for URI %q", prefix, uri))
}
if _, ok := ns.uriToPrefix[uri]; !ok {
ns.uriToPrefix[uri] = prefix
}
}
func invalidSchemaRune(r rune) bool {
return unicode.IsSpace(r)
}
// IsValidSchemaURI reports whether the given argument is suitable for
// use as a namespace schema URI. It must be non-empty, a valid UTF-8
// string and it must not contain white space.
func IsValidSchemaURI(uri string) bool {
// TODO more stringent requirements?
return len(uri) > 0 &&
utf8.ValidString(uri) &&
strings.IndexFunc(uri, invalidSchemaRune) == -1
}
func invalidPrefixRune(r rune) bool {
return r == ' ' || r == ':' || unicode.IsSpace(r)
}
func IsValidPrefix(prefix string) bool {
return utf8.ValidString(prefix) && strings.IndexFunc(prefix, invalidPrefixRune) == -1
}

View file

@ -1,97 +0,0 @@
package checkers
import (
"context"
"fmt"
"time"
"gopkg.in/errgo.v1"
"gopkg.in/macaroon.v2"
)
// Clock represents a clock that can be faked for testing purposes.
type Clock interface {
Now() time.Time
}
type timeKey struct{}
func ContextWithClock(ctx context.Context, clock Clock) context.Context {
if clock == nil {
return ctx
}
return context.WithValue(ctx, timeKey{}, clock)
}
func clockFromContext(ctx context.Context) Clock {
c, _ := ctx.Value(timeKey{}).(Clock)
return c
}
func checkTimeBefore(ctx context.Context, _, arg string) error {
var now time.Time
if clock := clockFromContext(ctx); clock != nil {
now = clock.Now()
} else {
now = time.Now()
}
t, err := time.Parse(time.RFC3339Nano, arg)
if err != nil {
return errgo.Mask(err)
}
if !now.Before(t) {
return fmt.Errorf("macaroon has expired")
}
return nil
}
// TimeBeforeCaveat returns a caveat that specifies that
// the time that it is checked should be before t.
func TimeBeforeCaveat(t time.Time) Caveat {
return firstParty(CondTimeBefore, t.UTC().Format(time.RFC3339Nano))
}
// ExpiryTime returns the minimum time of any time-before caveats found
// in the given slice and whether there were any such caveats found.
//
// The ns parameter is used to determine the standard namespace prefix - if
// the standard namespace is not found, the empty prefix is assumed.
func ExpiryTime(ns *Namespace, cavs []macaroon.Caveat) (time.Time, bool) {
prefix, _ := ns.Resolve(StdNamespace)
timeBeforeCond := ConditionWithPrefix(prefix, CondTimeBefore)
var t time.Time
var expires bool
for _, cav := range cavs {
cav := string(cav.Id)
name, rest, _ := ParseCaveat(cav)
if name != timeBeforeCond {
continue
}
et, err := time.Parse(time.RFC3339Nano, rest)
if err != nil {
continue
}
if !expires || et.Before(t) {
t = et
expires = true
}
}
return t, expires
}
// MacaroonsExpiryTime returns the minimum time of any time-before
// caveats found in the given macaroons and whether there were
// any such caveats found.
func MacaroonsExpiryTime(ns *Namespace, ms macaroon.Slice) (time.Time, bool) {
var t time.Time
var expires bool
for _, m := range ms {
if et, ex := ExpiryTime(ns, m.Caveats()); ex {
if !expires || et.Before(t) {
t = et
expires = true
}
}
}
return t, expires
}

View file

@ -1,381 +0,0 @@
package bakery
import (
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/binary"
"encoding/json"
"golang.org/x/crypto/nacl/box"
"gopkg.in/errgo.v1"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
)
type caveatRecord struct {
RootKey []byte
Condition string
}
// caveatJSON defines the format of a V1 JSON-encoded third party caveat id.
type caveatJSON struct {
ThirdPartyPublicKey *PublicKey
FirstPartyPublicKey *PublicKey
Nonce []byte
Id string
}
// encodeCaveat encrypts a third-party caveat with the given condtion
// and root key. The thirdPartyInfo key holds information about the
// third party we're encrypting the caveat for; the key is the
// public/private key pair of the party that's adding the caveat.
//
// The caveat will be encoded according to the version information
// found in thirdPartyInfo.
func encodeCaveat(
condition string,
rootKey []byte,
thirdPartyInfo ThirdPartyInfo,
key *KeyPair,
ns *checkers.Namespace,
) ([]byte, error) {
switch thirdPartyInfo.Version {
case Version0, Version1:
return encodeCaveatV1(condition, rootKey, &thirdPartyInfo.PublicKey, key)
case Version2:
return encodeCaveatV2(condition, rootKey, &thirdPartyInfo.PublicKey, key)
default:
// Version 3 or later - use V3.
return encodeCaveatV3(condition, rootKey, &thirdPartyInfo.PublicKey, key, ns)
}
}
// encodeCaveatV1 creates a JSON-encoded third-party caveat
// with the given condtion and root key. The thirdPartyPubKey key
// represents the public key of the third party we're encrypting
// the caveat for; the key is the public/private key pair of the party
// that's adding the caveat.
func encodeCaveatV1(
condition string,
rootKey []byte,
thirdPartyPubKey *PublicKey,
key *KeyPair,
) ([]byte, error) {
var nonce [NonceLen]byte
if _, err := rand.Read(nonce[:]); err != nil {
return nil, errgo.Notef(err, "cannot generate random number for nonce")
}
plain := caveatRecord{
RootKey: rootKey,
Condition: condition,
}
plainData, err := json.Marshal(&plain)
if err != nil {
return nil, errgo.Notef(err, "cannot marshal %#v", &plain)
}
sealed := box.Seal(nil, plainData, &nonce, thirdPartyPubKey.boxKey(), key.Private.boxKey())
id := caveatJSON{
ThirdPartyPublicKey: thirdPartyPubKey,
FirstPartyPublicKey: &key.Public,
Nonce: nonce[:],
Id: base64.StdEncoding.EncodeToString(sealed),
}
data, err := json.Marshal(id)
if err != nil {
return nil, errgo.Notef(err, "cannot marshal %#v", id)
}
buf := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
base64.StdEncoding.Encode(buf, data)
return buf, nil
}
// encodeCaveatV2 creates a version 2 third-party caveat.
func encodeCaveatV2(
condition string,
rootKey []byte,
thirdPartyPubKey *PublicKey,
key *KeyPair,
) ([]byte, error) {
return encodeCaveatV2V3(Version2, condition, rootKey, thirdPartyPubKey, key, nil)
}
// encodeCaveatV3 creates a version 3 third-party caveat.
func encodeCaveatV3(
condition string,
rootKey []byte,
thirdPartyPubKey *PublicKey,
key *KeyPair,
ns *checkers.Namespace,
) ([]byte, error) {
return encodeCaveatV2V3(Version3, condition, rootKey, thirdPartyPubKey, key, ns)
}
const publicKeyPrefixLen = 4
// version3CaveatMinLen holds an underestimate of the
// minimum length of a version 3 caveat.
const version3CaveatMinLen = 1 + 4 + 32 + 24 + box.Overhead + 1
// encodeCaveatV3 creates a version 2 or version 3 third-party caveat.
//
// The format has the following packed binary fields (note
// that all fields up to and including the nonce are the same
// as the v2 format):
//
// version 2 or 3 [1 byte]
// first 4 bytes of third-party Curve25519 public key [4 bytes]
// first-party Curve25519 public key [32 bytes]
// nonce [24 bytes]
// encrypted secret part [rest of message]
//
// The encrypted part encrypts the following fields
// with box.Seal:
//
// version 2 or 3 [1 byte]
// length of root key [n: uvarint]
// root key [n bytes]
// length of encoded namespace [n: uvarint] (Version 3 only)
// encoded namespace [n bytes] (Version 3 only)
// condition [rest of encrypted part]
func encodeCaveatV2V3(
version Version,
condition string,
rootKey []byte,
thirdPartyPubKey *PublicKey,
key *KeyPair,
ns *checkers.Namespace,
) ([]byte, error) {
var nsData []byte
if version >= Version3 {
data, err := ns.MarshalText()
if err != nil {
return nil, errgo.Mask(err)
}
nsData = data
}
// dataLen is our estimate of how long the data will be.
// As we always use append, this doesn't have to be strictly
// accurate but it's nice to avoid allocations.
dataLen := 0 +
1 + // version
publicKeyPrefixLen +
KeyLen +
NonceLen +
box.Overhead +
1 + // version
uvarintLen(uint64(len(rootKey))) +
len(rootKey) +
uvarintLen(uint64(len(nsData))) +
len(nsData) +
len(condition)
var nonce [NonceLen]byte = uuidGen.Next()
data := make([]byte, 0, dataLen)
data = append(data, byte(version))
data = append(data, thirdPartyPubKey.Key[:publicKeyPrefixLen]...)
data = append(data, key.Public.Key[:]...)
data = append(data, nonce[:]...)
secret := encodeSecretPartV2V3(version, condition, rootKey, nsData)
return box.Seal(data, secret, &nonce, thirdPartyPubKey.boxKey(), key.Private.boxKey()), nil
}
// encodeSecretPartV2V3 creates a version 2 or version 3 secret part of the third party
// caveat. The returned data is not encrypted.
//
// The format has the following packed binary fields:
// version 2 or 3 [1 byte]
// root key length [n: uvarint]
// root key [n bytes]
// namespace length [n: uvarint] (v3 only)
// namespace [n bytes] (v3 only)
// predicate [rest of message]
func encodeSecretPartV2V3(version Version, condition string, rootKey, nsData []byte) []byte {
data := make([]byte, 0, 1+binary.MaxVarintLen64+len(rootKey)+len(condition))
data = append(data, byte(version)) // version
data = appendUvarint(data, uint64(len(rootKey)))
data = append(data, rootKey...)
if version >= Version3 {
data = appendUvarint(data, uint64(len(nsData)))
data = append(data, nsData...)
}
data = append(data, condition...)
return data
}
// decodeCaveat attempts to decode caveat by decrypting the encrypted part
// using key.
func decodeCaveat(key *KeyPair, caveat []byte) (*ThirdPartyCaveatInfo, error) {
if len(caveat) == 0 {
return nil, errgo.New("empty third party caveat")
}
switch caveat[0] {
case byte(Version2):
return decodeCaveatV2V3(Version2, key, caveat)
case byte(Version3):
if len(caveat) < version3CaveatMinLen {
// If it has the version 3 caveat tag and it's too short, it's
// almost certainly an id, not an encrypted payload.
return nil, errgo.Newf("caveat id payload not provided for caveat id %q", caveat)
}
return decodeCaveatV2V3(Version3, key, caveat)
case 'e':
// 'e' will be the first byte if the caveatid is a base64 encoded JSON object.
return decodeCaveatV1(key, caveat)
default:
return nil, errgo.Newf("caveat has unsupported version %d", caveat[0])
}
}
// decodeCaveatV1 attempts to decode a base64 encoded JSON id. This
// encoding is nominally version -1.
func decodeCaveatV1(key *KeyPair, caveat []byte) (*ThirdPartyCaveatInfo, error) {
data := make([]byte, (3*len(caveat)+3)/4)
n, err := base64.StdEncoding.Decode(data, caveat)
if err != nil {
return nil, errgo.Notef(err, "cannot base64-decode caveat")
}
data = data[:n]
var wrapper caveatJSON
if err := json.Unmarshal(data, &wrapper); err != nil {
return nil, errgo.Notef(err, "cannot unmarshal caveat %q", data)
}
if !bytes.Equal(key.Public.Key[:], wrapper.ThirdPartyPublicKey.Key[:]) {
return nil, errgo.New("public key mismatch")
}
if wrapper.FirstPartyPublicKey == nil {
return nil, errgo.New("target service public key not specified")
}
// The encrypted string is base64 encoded in the JSON representation.
secret, err := base64.StdEncoding.DecodeString(wrapper.Id)
if err != nil {
return nil, errgo.Notef(err, "cannot base64-decode encrypted data")
}
var nonce [NonceLen]byte
if copy(nonce[:], wrapper.Nonce) < NonceLen {
return nil, errgo.Newf("nonce too short %x", wrapper.Nonce)
}
c, ok := box.Open(nil, secret, &nonce, wrapper.FirstPartyPublicKey.boxKey(), key.Private.boxKey())
if !ok {
return nil, errgo.Newf("cannot decrypt caveat %#v", wrapper)
}
var record caveatRecord
if err := json.Unmarshal(c, &record); err != nil {
return nil, errgo.Notef(err, "cannot decode third party caveat record")
}
return &ThirdPartyCaveatInfo{
Condition: []byte(record.Condition),
FirstPartyPublicKey: *wrapper.FirstPartyPublicKey,
ThirdPartyKeyPair: *key,
RootKey: record.RootKey,
Caveat: caveat,
Version: Version1,
Namespace: legacyNamespace(),
}, nil
}
// decodeCaveatV2V3 decodes a version 2 or version 3 caveat.
func decodeCaveatV2V3(version Version, key *KeyPair, caveat []byte) (*ThirdPartyCaveatInfo, error) {
origCaveat := caveat
if len(caveat) < 1+publicKeyPrefixLen+KeyLen+NonceLen+box.Overhead {
return nil, errgo.New("caveat id too short")
}
caveat = caveat[1:] // skip version (already checked)
publicKeyPrefix, caveat := caveat[:publicKeyPrefixLen], caveat[publicKeyPrefixLen:]
if !bytes.Equal(key.Public.Key[:publicKeyPrefixLen], publicKeyPrefix) {
return nil, errgo.New("public key mismatch")
}
var firstPartyPub PublicKey
copy(firstPartyPub.Key[:], caveat[:KeyLen])
caveat = caveat[KeyLen:]
var nonce [NonceLen]byte
copy(nonce[:], caveat[:NonceLen])
caveat = caveat[NonceLen:]
data, ok := box.Open(nil, caveat, &nonce, firstPartyPub.boxKey(), key.Private.boxKey())
if !ok {
return nil, errgo.Newf("cannot decrypt caveat id")
}
rootKey, ns, condition, err := decodeSecretPartV2V3(version, data)
if err != nil {
return nil, errgo.Notef(err, "invalid secret part")
}
return &ThirdPartyCaveatInfo{
Condition: condition,
FirstPartyPublicKey: firstPartyPub,
ThirdPartyKeyPair: *key,
RootKey: rootKey,
Caveat: origCaveat,
Version: version,
Namespace: ns,
}, nil
}
func decodeSecretPartV2V3(version Version, data []byte) (rootKey []byte, ns *checkers.Namespace, condition []byte, err error) {
fail := func(err error) ([]byte, *checkers.Namespace, []byte, error) {
return nil, nil, nil, err
}
if len(data) < 1 {
return fail(errgo.New("secret part too short"))
}
gotVersion, data := data[0], data[1:]
if version != Version(gotVersion) {
return fail(errgo.Newf("unexpected secret part version, got %d want %d", gotVersion, version))
}
l, n := binary.Uvarint(data)
if n <= 0 || uint64(n)+l > uint64(len(data)) {
return fail(errgo.Newf("invalid root key length"))
}
data = data[n:]
rootKey, data = data[:l], data[l:]
if version >= Version3 {
var nsData []byte
var ns1 checkers.Namespace
l, n = binary.Uvarint(data)
if n <= 0 || uint64(n)+l > uint64(len(data)) {
return fail(errgo.Newf("invalid namespace length"))
}
data = data[n:]
nsData, data = data[:l], data[l:]
if err := ns1.UnmarshalText(nsData); err != nil {
return fail(errgo.Notef(err, "cannot unmarshal namespace"))
}
ns = &ns1
} else {
ns = legacyNamespace()
}
return rootKey, ns, data, nil
}
// appendUvarint appends n to data encoded as a variable-length
// unsigned integer.
func appendUvarint(data []byte, n uint64) []byte {
// Ensure the capacity is sufficient. If our space calculations when
// allocating data were correct, this should never happen,
// but be defensive just in case.
for need := uvarintLen(n); cap(data)-len(data) < need; {
data1 := append(data[0:cap(data)], 0)
data = data1[0:len(data)]
}
nlen := binary.PutUvarint(data[len(data):cap(data)], n)
return data[0 : len(data)+nlen]
}
// uvarintLen returns the number of bytes that n will require
// when encoded with binary.PutUvarint.
func uvarintLen(n uint64) int {
len := 1
n >>= 7
for ; n > 0; n >>= 7 {
len++
}
return len
}

View file

@ -1,282 +0,0 @@
package bakery
import (
"context"
"crypto/rand"
"fmt"
"strconv"
"strings"
"gopkg.in/errgo.v1"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
)
// LocalThirdPartyCaveat returns a third-party caveat that, when added
// to a macaroon with AddCaveat, results in a caveat
// with the location "local", encrypted with the given public key.
// This can be automatically discharged by DischargeAllWithKey.
func LocalThirdPartyCaveat(key *PublicKey, version Version) checkers.Caveat {
var loc string
if version < Version2 {
loc = "local " + key.String()
} else {
loc = fmt.Sprintf("local %d %s", version, key)
}
return checkers.Caveat{
Location: loc,
}
}
// parseLocalLocation parses a local caveat location as generated by
// LocalThirdPartyCaveat. This is of the form:
//
// local <version> <pubkey>
//
// where <version> is the bakery version of the client that we're
// adding the local caveat for.
//
// It returns false if the location does not represent a local
// caveat location.
func parseLocalLocation(loc string) (ThirdPartyInfo, bool) {
if !strings.HasPrefix(loc, "local ") {
return ThirdPartyInfo{}, false
}
version := Version1
fields := strings.Fields(loc)
fields = fields[1:] // Skip "local"
switch len(fields) {
case 2:
v, err := strconv.Atoi(fields[0])
if err != nil {
return ThirdPartyInfo{}, false
}
version = Version(v)
fields = fields[1:]
fallthrough
case 1:
var key PublicKey
if err := key.UnmarshalText([]byte(fields[0])); err != nil {
return ThirdPartyInfo{}, false
}
return ThirdPartyInfo{
PublicKey: key,
Version: version,
}, true
default:
return ThirdPartyInfo{}, false
}
}
// DischargeParams holds parameters for a Discharge call.
type DischargeParams struct {
// Id holds the id to give to the discharge macaroon.
// If Caveat is empty, then the id also holds the
// encrypted third party caveat.
Id []byte
// Caveat holds the encrypted third party caveat. If this
// is nil, Id will be used.
Caveat []byte
// Key holds the key to use to decrypt the third party
// caveat information and to encrypt any additional
// third party caveats returned by the caveat checker.
Key *KeyPair
// Checker is used to check the third party caveat,
// and may also return further caveats to be added to
// the discharge macaroon.
Checker ThirdPartyCaveatChecker
// Locator is used to information on third parties
// referred to by third party caveats returned by the Checker.
Locator ThirdPartyLocator
}
// Discharge creates a macaroon to discharges a third party caveat.
// The given parameters specify the caveat and how it should be checked/
//
// The condition implicit in the caveat is checked for validity using p.Checker. If
// it is valid, a new macaroon is returned which discharges the caveat.
//
// The macaroon is created with a version derived from the version
// that was used to encode the id.
func Discharge(ctx context.Context, p DischargeParams) (*Macaroon, error) {
var caveatIdPrefix []byte
if p.Caveat == nil {
// The caveat information is encoded in the id itself.
p.Caveat = p.Id
} else {
// We've been given an explicit id, so when extra third party
// caveats are added, use that id as the prefix
// for any more ids.
caveatIdPrefix = p.Id
}
cavInfo, err := decodeCaveat(p.Key, p.Caveat)
if err != nil {
return nil, errgo.Notef(err, "discharger cannot decode caveat id")
}
cavInfo.Id = p.Id
// Note that we don't check the error - we allow the
// third party checker to see even caveats that we can't
// understand.
cond, arg, _ := checkers.ParseCaveat(string(cavInfo.Condition))
var caveats []checkers.Caveat
if cond == checkers.CondNeedDeclared {
cavInfo.Condition = []byte(arg)
caveats, err = checkNeedDeclared(ctx, cavInfo, p.Checker)
} else {
caveats, err = p.Checker.CheckThirdPartyCaveat(ctx, cavInfo)
}
if err != nil {
return nil, errgo.Mask(err, errgo.Any)
}
// Note that the discharge macaroon does not need to
// be stored persistently. Indeed, it would be a problem if
// we did, because then the macaroon could potentially be used
// for normal authorization with the third party.
m, err := NewMacaroon(cavInfo.RootKey, p.Id, "", cavInfo.Version, cavInfo.Namespace)
if err != nil {
return nil, errgo.Mask(err)
}
m.caveatIdPrefix = caveatIdPrefix
for _, cav := range caveats {
if err := m.AddCaveat(ctx, cav, p.Key, p.Locator); err != nil {
return nil, errgo.Notef(err, "could not add caveat")
}
}
return m, nil
}
func checkNeedDeclared(ctx context.Context, cavInfo *ThirdPartyCaveatInfo, checker ThirdPartyCaveatChecker) ([]checkers.Caveat, error) {
arg := string(cavInfo.Condition)
i := strings.Index(arg, " ")
if i <= 0 {
return nil, errgo.Newf("need-declared caveat requires an argument, got %q", arg)
}
needDeclared := strings.Split(arg[0:i], ",")
for _, d := range needDeclared {
if d == "" {
return nil, errgo.New("need-declared caveat with empty required attribute")
}
}
if len(needDeclared) == 0 {
return nil, fmt.Errorf("need-declared caveat with no required attributes")
}
cavInfo.Condition = []byte(arg[i+1:])
caveats, err := checker.CheckThirdPartyCaveat(ctx, cavInfo)
if err != nil {
return nil, errgo.Mask(err, errgo.Any)
}
declared := make(map[string]bool)
for _, cav := range caveats {
if cav.Location != "" {
continue
}
// Note that we ignore the error. We allow the service to
// generate caveats that we don't understand here.
cond, arg, _ := checkers.ParseCaveat(cav.Condition)
if cond != checkers.CondDeclared {
continue
}
parts := strings.SplitN(arg, " ", 2)
if len(parts) != 2 {
return nil, errgo.Newf("declared caveat has no value")
}
declared[parts[0]] = true
}
// Add empty declarations for everything mentioned in need-declared
// that was not actually declared.
for _, d := range needDeclared {
if !declared[d] {
caveats = append(caveats, checkers.DeclaredCaveat(d, ""))
}
}
return caveats, nil
}
func randomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
return nil, fmt.Errorf("cannot generate %d random bytes: %v", n, err)
}
return b, nil
}
// ThirdPartyCaveatInfo holds the information decoded from
// a third party caveat id.
type ThirdPartyCaveatInfo struct {
// Condition holds the third party condition to be discharged.
// This is the only field that most third party dischargers will
// need to consider.
Condition []byte
// FirstPartyPublicKey holds the public key of the party
// that created the third party caveat.
FirstPartyPublicKey PublicKey
// ThirdPartyKeyPair holds the key pair used to decrypt
// the caveat - the key pair of the discharging service.
ThirdPartyKeyPair KeyPair
// RootKey holds the secret root key encoded by the caveat.
RootKey []byte
// CaveatId holds the full encoded caveat id from which all
// the other fields are derived.
Caveat []byte
// Version holds the version that was used to encode
// the caveat id.
Version Version
// Id holds the id of the third party caveat (the id that
// the discharge macaroon should be given). This
// will differ from Caveat when the caveat information
// is encoded separately.
Id []byte
// Namespace holds the namespace of the first party
// that created the macaroon, as encoded by the party
// that added the third party caveat.
Namespace *checkers.Namespace
}
// ThirdPartyCaveatChecker holds a function that checks third party caveats
// for validity. If the caveat is valid, it returns a nil error and
// optionally a slice of extra caveats that will be added to the
// discharge macaroon. The caveatId parameter holds the still-encoded id
// of the caveat.
//
// If the caveat kind was not recognised, the checker should return an
// error with a ErrCaveatNotRecognized cause.
type ThirdPartyCaveatChecker interface {
CheckThirdPartyCaveat(ctx context.Context, info *ThirdPartyCaveatInfo) ([]checkers.Caveat, error)
}
// ThirdPartyCaveatCheckerFunc implements ThirdPartyCaveatChecker by calling a function.
type ThirdPartyCaveatCheckerFunc func(context.Context, *ThirdPartyCaveatInfo) ([]checkers.Caveat, error)
// CheckThirdPartyCaveat implements ThirdPartyCaveatChecker.CheckThirdPartyCaveat by calling
// the receiver with the given arguments
func (c ThirdPartyCaveatCheckerFunc) CheckThirdPartyCaveat(ctx context.Context, info *ThirdPartyCaveatInfo) ([]checkers.Caveat, error) {
return c(ctx, info)
}
// FirstPartyCaveatChecker is used to check first party caveats
// for validity with respect to information in the provided context.
//
// If the caveat kind was not recognised, the checker should return
// ErrCaveatNotRecognized.
type FirstPartyCaveatChecker interface {
// CheckFirstPartyCaveat checks that the given caveat condition
// is valid with respect to the given context information.
CheckFirstPartyCaveat(ctx context.Context, caveat string) error
// Namespace returns the namespace associated with the
// caveat checker.
Namespace() *checkers.Namespace
}

View file

@ -1,56 +0,0 @@
package bakery
import (
"context"
"gopkg.in/errgo.v1"
"gopkg.in/macaroon.v2"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
)
// DischargeAll gathers discharge macaroons for all the third party
// caveats in m (and any subsequent caveats required by those) using
// getDischarge to acquire each discharge macaroon. It returns a slice
// with m as the first element, followed by all the discharge macaroons.
// All the discharge macaroons will be bound to the primary macaroon.
//
// The getDischarge function is passed the caveat to be discharged;
// encryptedCaveat will be passed the external caveat payload found
// in m, if any.
func DischargeAll(
ctx context.Context,
m *Macaroon,
getDischarge func(ctx context.Context, cav macaroon.Caveat, encryptedCaveat []byte) (*Macaroon, error),
) (macaroon.Slice, error) {
return DischargeAllWithKey(ctx, m, getDischarge, nil)
}
// DischargeAllWithKey is like DischargeAll except that the localKey
// parameter may optionally hold the key of the client, in which case it
// will be used to discharge any third party caveats with the special
// location "local". In this case, the caveat itself must be "true". This
// can be used be a server to ask a client to prove ownership of the
// private key.
//
// When localKey is nil, DischargeAllWithKey is exactly the same as
// DischargeAll.
func DischargeAllWithKey(
ctx context.Context,
m *Macaroon,
getDischarge func(ctx context.Context, cav macaroon.Caveat, encodedCaveat []byte) (*Macaroon, error),
localKey *KeyPair,
) (macaroon.Slice, error) {
discharges, err := Slice{m}.DischargeAll(ctx, getDischarge, localKey)
if err != nil {
return nil, errgo.Mask(err, errgo.Any)
}
return discharges.Bind(), nil
}
var localDischargeChecker = ThirdPartyCaveatCheckerFunc(func(_ context.Context, info *ThirdPartyCaveatInfo) ([]checkers.Caveat, error) {
if string(info.Condition) != "true" {
return nil, checkers.ErrCaveatNotRecognized
}
return nil, nil
})

View file

@ -1,88 +0,0 @@
// The bakery package layers on top of the macaroon package, providing
// a transport and store-agnostic way of using macaroons to assert
// client capabilities.
//
// Summary
//
// The Bakery type is probably where you want to start.
// It encapsulates a Checker type, which performs checking
// of operations, and an Oven type, which encapsulates
// the actual details of the macaroon encoding conventions.
//
// Most other types and functions are designed either to plug
// into one of the above types (the various Authorizer
// implementations, for example), or to expose some independent
// functionality that's potentially useful (Discharge, for example).
//
// The rest of this introduction introduces some of the concepts
// used by the bakery package.
//
// Identity and entities
//
// An Identity represents some authenticated user (or agent), usually
// the client in a network protocol. An identity can be authenticated by
// an external identity server (with a third party macaroon caveat) or
// by locally provided information such as a username and password.
//
// The Checker type is not responsible for determining identity - that
// functionality is represented by the IdentityClient interface.
//
// The Checker uses identities to decide whether something should be
// allowed or not - the Authorizer interface is used to ask whether a
// given identity should be allowed to perform some set of operations.
//
// Operations
//
// An operation defines some requested action on an entity. For example,
// if file system server defines an entity for every file in the server,
// an operation to read a file might look like:
//
// Op{
// Entity: "/foo",
// Action: "write",
// }
//
// The exact set of entities and actions is up to the caller, but should
// be kept stable over time because authorization tokens will contain
// these names.
//
// To authorize some request on behalf of a remote user, first find out
// what operations that request needs to perform. For example, if the
// user tries to delete a file, the entity might be the path to the
// file's directory and the action might be "write". It may often be
// possible to determine the operations required by a request without
// reference to anything external, when the request itself contains all
// the necessary information.
//
// The LoginOp operation is special - any macaroon associated with this
// operation is treated as a bearer of identity information. If two
// valid LoginOp macaroons are presented, only the first one will be
// used for identity.
//
// Authorization
//
// The Authorizer interface is responsible for determining whether a
// given authenticated identity is authorized to perform a set of
// operations. This is used when the macaroons provided to Auth are not
// sufficient to authorize the operations themselves.
//
// Capabilities
//
// A "capability" is represented by a macaroon that's associated with
// one or more operations, and grants the capability to perform all
// those operations. The AllowCapability method reports whether a
// capability is allowed. It takes into account any authenticated
// identity and any other capabilities provided.
//
// Third party caveats
//
// Sometimes authorization will only be granted if a third party caveat
// is discharged. This will happen when an IdentityClient or Authorizer
// returns a third party caveat.
//
// When this happens, a DischargeRequiredError will be returned
// containing the caveats and the operations required. The caller is
// responsible for creating a macaroon with those caveats associated
// with those operations and for passing that macaroon to the client to
// discharge.
package bakery

View file

@ -1,77 +0,0 @@
package bakery
import (
"fmt"
"gopkg.in/errgo.v1"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
)
var (
// ErrNotFound is returned by Store.Get implementations
// to signal that an id has not been found.
ErrNotFound = errgo.New("not found")
// ErrPermissionDenied is returned from AuthChecker when
// permission has been denied.
ErrPermissionDenied = errgo.New("permission denied")
)
// DischargeRequiredError is returned when authorization has failed and a
// discharged macaroon might fix it.
//
// A caller should grant the user the ability to authorize by minting a
// macaroon associated with Ops (see MacaroonStore.MacaroonIdInfo for
// how the associated operations are retrieved) and adding Caveats. If
// the user succeeds in discharging the caveats, the authorization will
// be granted.
type DischargeRequiredError struct {
// Message holds some reason why the authorization was denied.
// TODO this is insufficient (and maybe unnecessary) because we
// can have multiple errors.
Message string
// Ops holds all the operations that were not authorized.
// If Ops contains a single LoginOp member, the macaroon
// should be treated as an login token. Login tokens (also
// known as authentication macaroons) usually have a longer
// life span than other macaroons.
Ops []Op
// Caveats holds the caveats that must be added
// to macaroons that authorize the above operations.
Caveats []checkers.Caveat
// ForAuthentication holds whether the macaroon holding
// the discharges will be used for authentication, and hence
// should have wider scope and longer lifetime.
// The bakery package never sets this field, but bakery/identchecker
// uses it.
ForAuthentication bool
}
func (e *DischargeRequiredError) Error() string {
return "macaroon discharge required: " + e.Message
}
func IsDischargeRequiredError(err error) bool {
_, ok := err.(*DischargeRequiredError)
return ok
}
// VerificationError is used to signify that an error is because
// of a verification failure rather than because verification
// could not be done.
type VerificationError struct {
Reason error
}
func (e *VerificationError) Error() string {
return fmt.Sprintf("verification failed: %v", e.Reason)
}
func isVerificationError(err error) bool {
_, ok := errgo.Cause(err).(*VerificationError)
return ok
}

View file

@ -1,219 +0,0 @@
package bakery
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"strings"
"sync"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/nacl/box"
"gopkg.in/errgo.v1"
"gopkg.in/macaroon.v2"
)
// KeyLen is the byte length of the Ed25519 public and private keys used for
// caveat id encryption.
const KeyLen = 32
// NonceLen is the byte length of the nonce values used for caveat id
// encryption.
const NonceLen = 24
// PublicKey is a 256-bit Ed25519 public key.
type PublicKey struct {
Key
}
// PrivateKey is a 256-bit Ed25519 private key.
type PrivateKey struct {
Key
}
// Public derives the public key from a private key.
func (k PrivateKey) Public() PublicKey {
var pub PublicKey
curve25519.ScalarBaseMult((*[32]byte)(&pub.Key), (*[32]byte)(&k.Key))
return pub
}
// Key is a 256-bit Ed25519 key.
type Key [KeyLen]byte
// String returns the base64 representation of the key.
func (k Key) String() string {
return base64.StdEncoding.EncodeToString(k[:])
}
// MarshalBinary implements encoding.BinaryMarshaler.MarshalBinary.
func (k Key) MarshalBinary() ([]byte, error) {
return k[:], nil
}
// isZero reports whether the key consists entirely of zeros.
func (k Key) isZero() bool {
return k == Key{}
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler.UnmarshalBinary.
func (k *Key) UnmarshalBinary(data []byte) error {
if len(data) != len(k) {
return errgo.Newf("wrong length for key, got %d want %d", len(data), len(k))
}
copy(k[:], data)
return nil
}
// MarshalText implements encoding.TextMarshaler.MarshalText.
func (k Key) MarshalText() ([]byte, error) {
data := make([]byte, base64.StdEncoding.EncodedLen(len(k)))
base64.StdEncoding.Encode(data, k[:])
return data, nil
}
// boxKey returns the box package's type for a key.
func (k Key) boxKey() *[KeyLen]byte {
return (*[KeyLen]byte)(&k)
}
// UnmarshalText implements encoding.TextUnmarshaler.UnmarshalText.
func (k *Key) UnmarshalText(text []byte) error {
data, err := macaroon.Base64Decode(text)
if err != nil {
return errgo.Notef(err, "cannot decode base64 key")
}
if len(data) != len(k) {
return errgo.Newf("wrong length for key, got %d want %d", len(data), len(k))
}
copy(k[:], data)
return nil
}
// ThirdPartyInfo holds information on a given third party
// discharge service.
type ThirdPartyInfo struct {
// PublicKey holds the public key of the third party.
PublicKey PublicKey
// Version holds latest the bakery protocol version supported
// by the discharger.
Version Version
}
// ThirdPartyLocator is used to find information on third
// party discharge services.
type ThirdPartyLocator interface {
// ThirdPartyInfo returns information on the third
// party at the given location. It returns ErrNotFound if no match is found.
// This method must be safe to call concurrently.
ThirdPartyInfo(ctx context.Context, loc string) (ThirdPartyInfo, error)
}
// ThirdPartyStore implements a simple ThirdPartyLocator.
// A trailing slash on locations is ignored.
type ThirdPartyStore struct {
mu sync.RWMutex
m map[string]ThirdPartyInfo
}
// NewThirdPartyStore returns a new instance of ThirdPartyStore
// that stores locations in memory.
func NewThirdPartyStore() *ThirdPartyStore {
return &ThirdPartyStore{
m: make(map[string]ThirdPartyInfo),
}
}
// AddInfo associates the given information with the
// given location, ignoring any trailing slash.
// This method is OK to call concurrently with sThirdPartyInfo.
func (s *ThirdPartyStore) AddInfo(loc string, info ThirdPartyInfo) {
s.mu.Lock()
defer s.mu.Unlock()
s.m[canonicalLocation(loc)] = info
}
func canonicalLocation(loc string) string {
return strings.TrimSuffix(loc, "/")
}
// ThirdPartyInfo implements the ThirdPartyLocator interface.
func (s *ThirdPartyStore) ThirdPartyInfo(ctx context.Context, loc string) (ThirdPartyInfo, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if info, ok := s.m[canonicalLocation(loc)]; ok {
return info, nil
}
return ThirdPartyInfo{}, ErrNotFound
}
// KeyPair holds a public/private pair of keys.
type KeyPair struct {
Public PublicKey `json:"public"`
Private PrivateKey `json:"private"`
}
// UnmarshalJSON implements json.Unmarshaler.
func (k *KeyPair) UnmarshalJSON(data []byte) error {
type keyPair KeyPair
if err := json.Unmarshal(data, (*keyPair)(k)); err != nil {
return err
}
return k.validate()
}
// UnmarshalYAML implements yaml.Unmarshaler.
func (k *KeyPair) UnmarshalYAML(unmarshal func(interface{}) error) error {
type keyPair KeyPair
if err := unmarshal((*keyPair)(k)); err != nil {
return err
}
return k.validate()
}
func (k *KeyPair) validate() error {
if k.Public.isZero() {
return errgo.Newf("missing public key")
}
if k.Private.isZero() {
return errgo.Newf("missing private key")
}
return nil
}
// GenerateKey generates a new key pair.
func GenerateKey() (*KeyPair, error) {
var key KeyPair
pub, priv, err := box.GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
key.Public = PublicKey{*pub}
key.Private = PrivateKey{*priv}
return &key, nil
}
// MustGenerateKey is like GenerateKey but panics if GenerateKey returns
// an error - useful in tests.
func MustGenerateKey() *KeyPair {
key, err := GenerateKey()
if err != nil {
panic(errgo.Notef(err, "cannot generate key"))
}
return key
}
// String implements the fmt.Stringer interface
// by returning the base64 representation of the
// public key part of key.
func (key *KeyPair) String() string {
return key.Public.String()
}
type emptyLocator struct{}
func (emptyLocator) ThirdPartyInfo(context.Context, string) (ThirdPartyInfo, error) {
return ThirdPartyInfo{}, ErrNotFound
}

View file

@ -1,28 +0,0 @@
package bakery
import (
"context"
)
// Logger is used by the bakery to log informational messages
// about bakery operations.
type Logger interface {
Infof(ctx context.Context, f string, args ...interface{})
Debugf(ctx context.Context, f string, args ...interface{})
}
// DefaultLogger returns a Logger instance that does nothing.
//
// Deprecated: DefaultLogger exists for historical compatibility
// only. Previously it logged using github.com/juju/loggo.
func DefaultLogger(name string) Logger {
return nopLogger{}
}
type nopLogger struct{}
// Debugf implements Logger.Debugf.
func (nopLogger) Debugf(context.Context, string, ...interface{}) {}
// Debugf implements Logger.Infof.
func (nopLogger) Infof(context.Context, string, ...interface{}) {}

View file

@ -1,356 +0,0 @@
package bakery
import (
"bytes"
"context"
"encoding/base64"
"encoding/binary"
"encoding/json"
"gopkg.in/errgo.v1"
"gopkg.in/macaroon.v2"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
)
// legacyNamespace holds the standard namespace as used by
// pre-version3 macaroons.
func legacyNamespace() *checkers.Namespace {
ns := checkers.NewNamespace(nil)
ns.Register(checkers.StdNamespace, "")
return ns
}
// Macaroon represents an undischarged macaroon along with its first
// party caveat namespace and associated third party caveat information
// which should be passed to the third party when discharging a caveat.
type Macaroon struct {
// m holds the underlying macaroon.
m *macaroon.Macaroon
// version holds the version of the macaroon.
version Version
// caveatData maps from a third party caveat id to its
// associated information, usually public-key encrypted with the
// third party's public key.
//
// If version is less than Version3, this will always be nil,
// because clients prior to that version do not support
// macaroon-external caveat ids.
caveatData map[string][]byte
// namespace holds the first-party caveat namespace of the macaroon.
namespace *checkers.Namespace
// caveatIdPrefix holds the prefix to use for the ids of any third
// party caveats created. This can be set when Discharge creates a
// discharge macaroon.
caveatIdPrefix []byte
}
// NewLegacyMacaroon returns a new macaroon holding m.
// This should only be used when there's no alternative
// (for example when m has been unmarshaled
// from some alternative format).
func NewLegacyMacaroon(m *macaroon.Macaroon) (*Macaroon, error) {
v, err := bakeryVersion(m.Version())
if err != nil {
return nil, errgo.Mask(err)
}
return &Macaroon{
m: m,
version: v,
namespace: legacyNamespace(),
}, nil
}
type macaroonJSON struct {
Macaroon *macaroon.Macaroon `json:"m"`
Version Version `json:"v"`
// Note: CaveatData is encoded using URL-base64-encoded keys
// because JSON cannot deal with arbitrary byte sequences
// in its strings, and URL-base64 values to match the
// standard macaroon encoding.
CaveatData map[string]string `json:"cdata,omitempty"`
Namespace *checkers.Namespace `json:"ns"`
}
// Clone returns a copy of the macaroon. Note that the the new
// macaroon's namespace still points to the same underlying Namespace -
// copying the macaroon does not make a copy of the namespace.
func (m *Macaroon) Clone() *Macaroon {
m1 := *m
m1.m = m1.m.Clone()
m1.caveatData = make(map[string][]byte)
for id, data := range m.caveatData {
m1.caveatData[id] = data
}
return &m1
}
// MarshalJSON implements json.Marshaler by marshaling
// the macaroon into the original macaroon format if the
// version is earlier than Version3.
func (m *Macaroon) MarshalJSON() ([]byte, error) {
if m.version < Version3 {
if len(m.caveatData) > 0 {
return nil, errgo.Newf("cannot marshal pre-version3 macaroon with external caveat data")
}
return m.m.MarshalJSON()
}
caveatData := make(map[string]string)
for id, data := range m.caveatData {
caveatData[base64.RawURLEncoding.EncodeToString([]byte(id))] = base64.RawURLEncoding.EncodeToString(data)
}
return json.Marshal(macaroonJSON{
Macaroon: m.m,
Version: m.version,
CaveatData: caveatData,
Namespace: m.namespace,
})
}
// UnmarshalJSON implements json.Unmarshaler by unmarshaling in a
// backwardly compatible way - if provided with a previous macaroon
// version, it will unmarshal that too.
func (m *Macaroon) UnmarshalJSON(data []byte) error {
// First try with new data format.
var m1 macaroonJSON
if err := json.Unmarshal(data, &m1); err != nil {
// If we get an unmarshal error, we won't be able
// to unmarshal into the old format either, as extra fields
// are ignored.
return errgo.Mask(err)
}
if m1.Macaroon == nil {
return m.unmarshalJSONOldFormat(data)
}
// We've got macaroon field - it's the new format.
if m1.Version < Version3 || m1.Version > LatestVersion {
return errgo.Newf("unexpected bakery macaroon version; got %d want %d", m1.Version, Version3)
}
if got, want := m1.Macaroon.Version(), MacaroonVersion(m1.Version); got != want {
return errgo.Newf("underlying macaroon has inconsistent version; got %d want %d", got, want)
}
caveatData := make(map[string][]byte)
for id64, data64 := range m1.CaveatData {
id, err := macaroon.Base64Decode([]byte(id64))
if err != nil {
return errgo.Notef(err, "cannot decode caveat id")
}
data, err := macaroon.Base64Decode([]byte(data64))
if err != nil {
return errgo.Notef(err, "cannot decode caveat")
}
caveatData[string(id)] = data
}
m.caveatData = caveatData
m.m = m1.Macaroon
m.namespace = m1.Namespace
// TODO should we allow version > LatestVersion here?
m.version = m1.Version
return nil
}
// unmarshalJSONOldFormat unmarshals the data from an old format
// macaroon (without any external caveats or namespace).
func (m *Macaroon) unmarshalJSONOldFormat(data []byte) error {
// Try to unmarshal from the original format.
var m1 *macaroon.Macaroon
if err := json.Unmarshal(data, &m1); err != nil {
return errgo.Mask(err)
}
m2, err := NewLegacyMacaroon(m1)
if err != nil {
return errgo.Mask(err)
}
*m = *m2
return nil
}
// bakeryVersion returns a bakery version that corresponds to
// the macaroon version v. It is necessarily approximate because
// several bakery versions can correspond to a single macaroon
// version, so it's only of use when decoding legacy formats
// (in Macaroon.UnmarshalJSON).
//
// It will return an error if it doesn't recognize the version.
func bakeryVersion(v macaroon.Version) (Version, error) {
switch v {
case macaroon.V1:
// Use version 1 because we don't know of any existing
// version 0 clients.
return Version1, nil
case macaroon.V2:
// Note that this could also correspond to Version3, but
// this logic is explicitly for legacy versions.
return Version2, nil
default:
return 0, errgo.Newf("unknown macaroon version when legacy-unmarshaling bakery macaroon; got %d", v)
}
}
// NewMacaroon creates and returns a new macaroon with the given root
// key, id and location. If the version is more than the latest known
// version, the latest known version will be used. The namespace is that
// of the service creating it.
func NewMacaroon(rootKey, id []byte, location string, version Version, ns *checkers.Namespace) (*Macaroon, error) {
if version > LatestVersion {
version = LatestVersion
}
m, err := macaroon.New(rootKey, id, location, MacaroonVersion(version))
if err != nil {
return nil, errgo.Notef(err, "cannot create macaroon")
}
return &Macaroon{
m: m,
version: version,
namespace: ns,
}, nil
}
// M returns the underlying macaroon held within m.
func (m *Macaroon) M() *macaroon.Macaroon {
return m.m
}
// Version returns the bakery version of the first party
// that created the macaroon.
func (m *Macaroon) Version() Version {
return m.version
}
// Namespace returns the first party caveat namespace of the macaroon.
func (m *Macaroon) Namespace() *checkers.Namespace {
return m.namespace
}
// AddCaveats is a convenienced method that calls m.AddCaveat for each
// caveat in cavs.
func (m *Macaroon) AddCaveats(ctx context.Context, cavs []checkers.Caveat, key *KeyPair, loc ThirdPartyLocator) error {
for _, cav := range cavs {
if err := m.AddCaveat(ctx, cav, key, loc); err != nil {
return errgo.Notef(err, "cannot add caveat %#v", cav)
}
}
return nil
}
// AddCaveat adds a caveat to the given macaroon.
//
// If it's a third-party caveat, it encrypts it using the given key pair
// and by looking up the location using the given locator. If it's a
// first party cavat, key and loc are unused.
//
// As a special case, if the caveat's Location field has the prefix
// "local " the caveat is added as a client self-discharge caveat using
// the public key base64-encoded in the rest of the location. In this
// case, the Condition field must be empty. The resulting third-party
// caveat will encode the condition "true" encrypted with that public
// key. See LocalThirdPartyCaveat for a way of creating such caveats.
func (m *Macaroon) AddCaveat(ctx context.Context, cav checkers.Caveat, key *KeyPair, loc ThirdPartyLocator) error {
if cav.Location == "" {
if err := m.m.AddFirstPartyCaveat([]byte(m.namespace.ResolveCaveat(cav).Condition)); err != nil {
return errgo.Mask(err)
}
return nil
}
if key == nil {
return errgo.Newf("no private key to encrypt third party caveat")
}
var info ThirdPartyInfo
if localInfo, ok := parseLocalLocation(cav.Location); ok {
info = localInfo
cav.Location = "local"
if cav.Condition != "" {
return errgo.New("cannot specify caveat condition in local third-party caveat")
}
cav.Condition = "true"
} else {
if loc == nil {
return errgo.Newf("no locator when adding third party caveat")
}
var err error
info, err = loc.ThirdPartyInfo(ctx, cav.Location)
if err != nil {
return errgo.Notef(err, "cannot find public key for location %q", cav.Location)
}
}
rootKey, err := randomBytes(24)
if err != nil {
return errgo.Notef(err, "cannot generate third party secret")
}
// Use the least supported version to encode the caveat.
if m.version < info.Version {
info.Version = m.version
}
caveatInfo, err := encodeCaveat(cav.Condition, rootKey, info, key, m.namespace)
if err != nil {
return errgo.Notef(err, "cannot create third party caveat at %q", cav.Location)
}
var id []byte
if info.Version < Version3 {
// We're encoding for an earlier client or third party which does
// not understand bundled caveat info, so use the encoded
// caveat information as the caveat id.
id = caveatInfo
} else {
id = m.newCaveatId(m.caveatIdPrefix)
if m.caveatData == nil {
m.caveatData = make(map[string][]byte)
}
m.caveatData[string(id)] = caveatInfo
}
if err := m.m.AddThirdPartyCaveat(rootKey, id, cav.Location); err != nil {
return errgo.Notef(err, "cannot add third party caveat")
}
return nil
}
// newCaveatId returns a third party caveat id that
// does not duplicate any third party caveat ids already inside m.
//
// If base is non-empty, it is used as the id prefix.
func (m *Macaroon) newCaveatId(base []byte) []byte {
var id []byte
if len(base) > 0 {
id = make([]byte, len(base), len(base)+binary.MaxVarintLen64)
copy(id, base)
} else {
id = make([]byte, 0, 1+binary.MaxVarintLen32)
// Add a version byte to the caveat id. Technically
// this is unnecessary as the caveat-decoding logic
// that looks at versions should never see this id,
// but if the caveat payload isn't provided with the
// payload, having this version gives a strong indication
// that the payload has been omitted so we can produce
// a better error for the user.
id = append(id, byte(Version3))
}
// Iterate through integers looking for one that isn't already used,
// starting from n so that if everyone is using this same algorithm,
// we'll only perform one iteration.
//
// Note that although this looks like an infinite loop,
// there's no way that it can run for more iterations
// than the total number of existing third party caveats,
// whatever their ids.
caveats := m.m.Caveats()
again:
for i := len(m.caveatData); ; i++ {
// We append a varint to the end of the id and assume that
// any client that's created the id that we're using as a base
// is using similar conventions - in the worst case they might
// end up with a duplicate third party caveat id and thus create
// a macaroon that cannot be discharged.
id1 := appendUvarint(id, uint64(i))
for _, cav := range caveats {
if cav.VerificationId != nil && bytes.Equal(cav.Id, id1) {
continue again
}
}
return id1
}
}

View file

@ -1,359 +0,0 @@
package bakery
import (
"bytes"
"context"
"encoding/base64"
"sort"
"github.com/go-macaroon-bakery/macaroonpb"
"github.com/rogpeppe/fastuuid"
"gopkg.in/errgo.v1"
"gopkg.in/macaroon.v2"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
)
// MacaroonVerifier verifies macaroons and returns the operations and
// caveats they're associated with.
type MacaroonVerifier interface {
// VerifyMacaroon verifies the signature of the given macaroon and returns
// information on its associated operations, and all the first party
// caveat conditions that need to be checked.
//
// This method should not check first party caveats itself.
//
// It should return a *VerificationError if the error occurred
// because the macaroon signature failed or the root key
// was not found - any other error will be treated as fatal
// by Checker and cause authorization to terminate.
VerifyMacaroon(ctx context.Context, ms macaroon.Slice) ([]Op, []string, error)
}
var uuidGen = fastuuid.MustNewGenerator()
// Oven bakes macaroons. They emerge sweet and delicious
// and ready for use in a Checker.
//
// All macaroons are associated with one or more operations (see
// the Op type) which define the capabilities of the macaroon.
//
// There is one special operation, "login" (defined by LoginOp)
// which grants the capability to speak for a particular user.
// The login capability will never be mixed with other capabilities.
//
// It is up to the caller to decide on semantics for other operations.
type Oven struct {
p OvenParams
}
type OvenParams struct {
// Namespace holds the namespace to use when adding first party caveats.
// If this is nil, checkers.New(nil).Namespace will be used.
Namespace *checkers.Namespace
// RootKeyStoreForEntity returns the macaroon storage to be
// used for root keys associated with macaroons created
// wth NewMacaroon.
//
// If this is nil, NewMemRootKeyStore will be used to create
// a new store to be used for all entities.
RootKeyStoreForOps func(ops []Op) RootKeyStore
// Key holds the private key pair used to encrypt third party caveats.
// If it is nil, no third party caveats can be created.
Key *KeyPair
// Location holds the location that will be associated with new macaroons
// (as returned by Macaroon.Location).
Location string
// Locator is used to find out information on third parties when
// adding third party caveats. If this is nil, no non-local third
// party caveats can be added.
Locator ThirdPartyLocator
// LegacyMacaroonOp holds the operation to associate with old
// macaroons that don't have associated operations.
// If this is empty, legacy macaroons will not be associated
// with any operations.
LegacyMacaroonOp Op
// TODO max macaroon or macaroon id size?
}
// NewOven returns a new oven using the given parameters.
func NewOven(p OvenParams) *Oven {
if p.Locator == nil {
p.Locator = emptyLocator{}
}
if p.RootKeyStoreForOps == nil {
store := NewMemRootKeyStore()
p.RootKeyStoreForOps = func(ops []Op) RootKeyStore {
return store
}
}
if p.Namespace == nil {
p.Namespace = checkers.New(nil).Namespace()
}
return &Oven{
p: p,
}
}
// VerifyMacaroon implements MacaroonVerifier.VerifyMacaroon, making Oven
// an instance of MacaroonVerifier.
//
// For macaroons minted with previous bakery versions, it always
// returns a single LoginOp operation.
func (o *Oven) VerifyMacaroon(ctx context.Context, ms macaroon.Slice) (ops []Op, conditions []string, err error) {
if len(ms) == 0 {
return nil, nil, errgo.Newf("no macaroons in slice")
}
storageId, ops, err := o.decodeMacaroonId(ms[0].Id())
if err != nil {
return nil, nil, errgo.Mask(err)
}
rootKey, err := o.p.RootKeyStoreForOps(ops).Get(ctx, storageId)
if err != nil {
if errgo.Cause(err) != ErrNotFound {
return nil, nil, errgo.Notef(err, "cannot get macaroon")
}
// If the macaroon was not found, it is probably
// because it's been removed after time-expiry,
// so return a verification error.
return nil, nil, &VerificationError{
Reason: errgo.Newf("macaroon not found in storage"),
}
}
conditions, err = ms[0].VerifySignature(rootKey, ms[1:])
if err != nil {
return nil, nil, &VerificationError{
Reason: errgo.Mask(err),
}
}
return ops, conditions, nil
}
func (o *Oven) decodeMacaroonId(id []byte) (storageId []byte, ops []Op, err error) {
base64Decoded := false
if id[0] == 'A' {
// The first byte is not a version number and it's 'A', which is the
// base64 encoding of the top 6 bits (all zero) of the version number 2 or 3,
// so we assume that it's the base64 encoding of a new-style
// macaroon id, so we base64 decode it.
//
// Note that old-style ids always start with an ASCII character >= 4
// (> 32 in fact) so this logic won't be triggered for those.
dec := make([]byte, base64.RawURLEncoding.DecodedLen(len(id)))
n, err := base64.RawURLEncoding.Decode(dec, id)
if err == nil {
// Set the id only on success - if it's a bad encoding, we'll get a not-found error
// which is fine because "not found" is a correct description of the issue - we
// can't find the root key for the given id.
id = dec[0:n]
base64Decoded = true
}
}
// Trim any extraneous information from the id before retrieving
// it from storage, including the UUID that's added when
// creating macaroons to make all macaroons unique even if
// they're using the same root key.
switch id[0] {
case byte(Version2):
// Skip the UUID at the start of the id.
storageId = id[1+16:]
case byte(Version3):
var id1 macaroonpb.MacaroonId
if err := id1.UnmarshalBinary(id[1:]); err != nil {
return nil, nil, errgo.Notef(err, "cannot unmarshal macaroon id")
}
if len(id1.Ops) == 0 || len(id1.Ops[0].Actions) == 0 {
return nil, nil, errgo.Newf("no operations found in macaroon")
}
ops = make([]Op, 0, len(id1.Ops))
for _, op := range id1.Ops {
for _, action := range op.Actions {
ops = append(ops, Op{
Entity: op.Entity,
Action: action,
})
}
}
return id1.StorageId, ops, nil
}
if !base64Decoded && isLowerCaseHexChar(id[0]) {
// It's an old-style id, probably with a hyphenated UUID.
// so trim that off.
if i := bytes.LastIndexByte(id, '-'); i >= 0 {
storageId = id[0:i]
}
}
if op := o.p.LegacyMacaroonOp; op != (Op{}) {
ops = []Op{op}
}
return storageId, ops, nil
}
// NewMacaroon takes a macaroon with the given version from the oven, associates it with the given operations
// and attaches the given caveats. There must be at least one operation specified.
func (o *Oven) NewMacaroon(ctx context.Context, version Version, caveats []checkers.Caveat, ops ...Op) (*Macaroon, error) {
if len(ops) == 0 {
return nil, errgo.Newf("cannot mint a macaroon associated with no operations")
}
ops = CanonicalOps(ops)
rootKey, storageId, err := o.p.RootKeyStoreForOps(ops).RootKey(ctx)
if err != nil {
return nil, errgo.Mask(err)
}
id, err := o.newMacaroonId(ctx, ops, storageId)
if err != nil {
return nil, errgo.Mask(err)
}
idBytesNoVersion, err := id.MarshalBinary()
if err != nil {
return nil, errgo.Mask(err)
}
idBytes := make([]byte, len(idBytesNoVersion)+1)
idBytes[0] = byte(LatestVersion)
// TODO We could use a proto.Buffer to avoid this copy.
copy(idBytes[1:], idBytesNoVersion)
if MacaroonVersion(version) < macaroon.V2 {
// The old macaroon format required valid text for the macaroon id,
// so base64-encode it.
b64data := make([]byte, base64.RawURLEncoding.EncodedLen(len(idBytes)))
base64.RawURLEncoding.Encode(b64data, idBytes)
idBytes = b64data
}
m, err := NewMacaroon(rootKey, idBytes, o.p.Location, version, o.p.Namespace)
if err != nil {
return nil, errgo.Notef(err, "cannot create macaroon with version %v", version)
}
if err := o.AddCaveats(ctx, m, caveats); err != nil {
return nil, errgo.Mask(err)
}
return m, nil
}
// AddCaveat adds a caveat to the given macaroon.
func (o *Oven) AddCaveat(ctx context.Context, m *Macaroon, cav checkers.Caveat) error {
return m.AddCaveat(ctx, cav, o.p.Key, o.p.Locator)
}
// AddCaveats adds all the caveats to the given macaroon.
func (o *Oven) AddCaveats(ctx context.Context, m *Macaroon, caveats []checkers.Caveat) error {
return m.AddCaveats(ctx, caveats, o.p.Key, o.p.Locator)
}
// Key returns the oven's private/public key par.
func (o *Oven) Key() *KeyPair {
return o.p.Key
}
// Locator returns the third party locator that the
// oven was created with.
func (o *Oven) Locator() ThirdPartyLocator {
return o.p.Locator
}
// CanonicalOps returns the given operations slice sorted
// with duplicates removed.
func CanonicalOps(ops []Op) []Op {
canonOps := opsByValue(ops)
needNewSlice := false
for i := 1; i < len(ops); i++ {
if !canonOps.Less(i-1, i) {
needNewSlice = true
break
}
}
if !needNewSlice {
return ops
}
canonOps = make([]Op, len(ops))
copy(canonOps, ops)
sort.Sort(canonOps)
// Note we know that there's at least one operation here
// because we'd have returned earlier if the slice was empty.
j := 0
for _, op := range canonOps[1:] {
if op != canonOps[j] {
j++
canonOps[j] = op
}
}
return canonOps[0 : j+1]
}
func (o *Oven) newMacaroonId(ctx context.Context, ops []Op, storageId []byte) (*macaroonpb.MacaroonId, error) {
uuid := uuidGen.Next()
nonce := uuid[0:16]
return &macaroonpb.MacaroonId{
Nonce: nonce,
StorageId: storageId,
Ops: macaroonIdOps(ops),
}, nil
}
// macaroonIdOps returns operations suitable for serializing
// as part of an *macaroonpb.MacaroonId. It assumes that
// ops has been canonicalized and that there's at least
// one operation.
func macaroonIdOps(ops []Op) []*macaroonpb.Op {
idOps := make([]macaroonpb.Op, 0, len(ops))
idOps = append(idOps, macaroonpb.Op{
Entity: ops[0].Entity,
Actions: []string{ops[0].Action},
})
i := 0
idOp := &idOps[0]
for _, op := range ops[1:] {
if op.Entity != idOp.Entity {
idOps = append(idOps, macaroonpb.Op{
Entity: op.Entity,
Actions: []string{op.Action},
})
i++
idOp = &idOps[i]
continue
}
if op.Action != idOp.Actions[len(idOp.Actions)-1] {
idOp.Actions = append(idOp.Actions, op.Action)
}
}
idOpPtrs := make([]*macaroonpb.Op, len(idOps))
for i := range idOps {
idOpPtrs[i] = &idOps[i]
}
return idOpPtrs
}
type opsByValue []Op
func (o opsByValue) Less(i, j int) bool {
o0, o1 := o[i], o[j]
if o0.Entity != o1.Entity {
return o0.Entity < o1.Entity
}
return o0.Action < o1.Action
}
func (o opsByValue) Swap(i, j int) {
o[i], o[j] = o[j], o[i]
}
func (o opsByValue) Len() int {
return len(o)
}
func isLowerCaseHexChar(c byte) bool {
switch {
case '0' <= c && c <= '9':
return true
case 'a' <= c && c <= 'f':
return true
}
return false
}

View file

@ -1,134 +0,0 @@
package bakery
import (
"context"
"fmt"
"time"
"gopkg.in/errgo.v1"
"gopkg.in/macaroon.v2"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
)
// Slice holds a slice of unbound macaroons.
type Slice []*Macaroon
// Bind prepares the macaroon slice for use in a request. This must be
// done before presenting the macaroons to a service for use as
// authorization tokens. The result will only be valid
// if s contains discharge macaroons for all third party
// caveats.
//
// All the macaroons in the returned slice will be copies
// of this in s, not references.
func (s Slice) Bind() macaroon.Slice {
if len(s) == 0 {
return nil
}
ms := make(macaroon.Slice, len(s))
ms[0] = s[0].M().Clone()
rootSig := ms[0].Signature()
for i, m := range s[1:] {
m1 := m.M().Clone()
m1.Bind(rootSig)
ms[i+1] = m1
}
return ms
}
// Purge returns a new slice holding all macaroons in s
// that expire after the given time.
func (ms Slice) Purge(t time.Time) Slice {
ms1 := make(Slice, 0, len(ms))
for i, m := range ms {
et, ok := checkers.ExpiryTime(m.Namespace(), m.M().Caveats())
if !ok || et.After(t) {
ms1 = append(ms1, m)
} else if i == 0 {
// The primary macaroon has expired, so all its discharges
// have expired too.
// TODO purge all discharge macaroons when the macaroon
// containing their third-party caveat expires.
return nil
}
}
return ms1
}
// DischargeAll discharges all the third party caveats in the slice for
// which discharge macaroons are not already present, using getDischarge
// to acquire the discharge macaroons. It always returns the slice with
// any acquired discharge macaroons added, even on error. It returns an
// error if all the discharges could not be acquired.
//
// Note that this differs from DischargeAll in that it can be given several existing
// discharges, and that the resulting discharges are not bound to the primary,
// so it's still possible to add caveats and reacquire expired discharges
// without reacquiring the primary macaroon.
func (ms Slice) DischargeAll(ctx context.Context, getDischarge func(ctx context.Context, cav macaroon.Caveat, encryptedCaveat []byte) (*Macaroon, error), localKey *KeyPair) (Slice, error) {
if len(ms) == 0 {
return nil, errgo.Newf("no macaroons to discharge")
}
ms1 := make(Slice, len(ms))
copy(ms1, ms)
// have holds the keys of all the macaroon ids in the slice.
type needCaveat struct {
// cav holds the caveat that needs discharge.
cav macaroon.Caveat
// encryptedCaveat holds encrypted caveat
// if it was held externally.
encryptedCaveat []byte
}
var need []needCaveat
have := make(map[string]bool)
for _, m := range ms[1:] {
have[string(m.M().Id())] = true
}
// addCaveats adds any required third party caveats to the need slice
// that aren't already present .
addCaveats := func(m *Macaroon) {
for _, cav := range m.M().Caveats() {
if len(cav.VerificationId) == 0 || have[string(cav.Id)] {
continue
}
need = append(need, needCaveat{
cav: cav,
encryptedCaveat: m.caveatData[string(cav.Id)],
})
}
}
for _, m := range ms {
addCaveats(m)
}
var errs []error
for len(need) > 0 {
cav := need[0]
need = need[1:]
var dm *Macaroon
var err error
if localKey != nil && cav.cav.Location == "local" {
// TODO use a small caveat id.
dm, err = Discharge(ctx, DischargeParams{
Key: localKey,
Checker: localDischargeChecker,
Caveat: cav.encryptedCaveat,
Id: cav.cav.Id,
Locator: emptyLocator{},
})
} else {
dm, err = getDischarge(ctx, cav.cav, cav.encryptedCaveat)
}
if err != nil {
errs = append(errs, errgo.NoteMask(err, fmt.Sprintf("cannot get discharge from %q", cav.cav.Location), errgo.Any))
continue
}
ms1 = append(ms1, dm)
addCaveats(dm)
}
if errs != nil {
// TODO log other errors? Return them all?
return ms1, errgo.Mask(errs[0], errgo.Any)
}
return ms1, nil
}

View file

@ -1,63 +0,0 @@
package bakery
import (
"context"
"sync"
)
// RootKeyStore defines store for macaroon root keys.
type RootKeyStore interface {
// Get returns the root key for the given id.
// If the item is not there, it returns ErrNotFound.
Get(ctx context.Context, id []byte) ([]byte, error)
// RootKey returns the root key to be used for making a new
// macaroon, and an id that can be used to look it up later with
// the Get method.
//
// Note that the root keys should remain available for as long
// as the macaroons using them are valid.
//
// Note that there is no need for it to return a new root key
// for every call - keys may be reused, although some key
// cycling is over time is advisable.
RootKey(ctx context.Context) (rootKey []byte, id []byte, err error)
}
// NewMemRootKeyStore returns an implementation of
// Store that generates a single key and always
// returns that from RootKey. The same id ("0") is always
// used.
func NewMemRootKeyStore() RootKeyStore {
return new(memRootKeyStore)
}
type memRootKeyStore struct {
mu sync.Mutex
key []byte
}
// Get implements Store.Get.
func (s *memRootKeyStore) Get(_ context.Context, id []byte) ([]byte, error) {
s.mu.Lock()
defer s.mu.Unlock()
if len(id) != 1 || id[0] != '0' || s.key == nil {
return nil, ErrNotFound
}
return s.key, nil
}
// RootKey implements Store.RootKey by always returning the same root
// key.
func (s *memRootKeyStore) RootKey(context.Context) (rootKey, id []byte, err error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.key == nil {
newKey, err := randomBytes(24)
if err != nil {
return nil, nil, err
}
s.key = newKey
}
return s.key, []byte("0"), nil
}

View file

@ -1,30 +0,0 @@
package bakery
import "gopkg.in/macaroon.v2"
// Version represents a version of the bakery protocol.
type Version int
const (
// In version 0, discharge-required errors use status 407
Version0 Version = 0
// In version 1, discharge-required errors use status 401.
Version1 Version = 1
// In version 2, binary macaroons and caveat ids are supported.
Version2 Version = 2
// In version 3, we support operations associated with macaroons
// and external third party caveats.
Version3 Version = 3
LatestVersion = Version3
)
// MacaroonVersion returns the macaroon version that should
// be used with the given bakery Version.
func MacaroonVersion(v Version) macaroon.Version {
switch v {
case Version0, Version1:
return macaroon.V1
default:
return macaroon.V2
}
}

View file

@ -1,200 +0,0 @@
package httpbakery
import (
"context"
"fmt"
"net/http"
"net/url"
"os"
"github.com/juju/webbrowser"
"gopkg.in/errgo.v1"
"gopkg.in/httprequest.v1"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery"
)
const WebBrowserInteractionKind = "browser-window"
// WaitTokenResponse holds the response type
// returned, JSON-encoded, from the waitToken
// URL passed to SetBrowserInteraction.
type WaitTokenResponse struct {
Kind string `json:"kind"`
// Token holds the token value when it's well-formed utf-8
Token string `json:"token,omitempty"`
// Token64 holds the token value, base64 encoded, when it's
// not well-formed utf-8.
Token64 string `json:"token64,omitempty"`
}
// WaitResponse holds the type that should be returned
// by an HTTP response made to a LegacyWaitURL
// (See the ErrorInfo type).
type WaitResponse struct {
Macaroon *bakery.Macaroon
}
// WebBrowserInteractionInfo holds the information
// expected in the browser-window interaction
// entry in an interaction-required error.
type WebBrowserInteractionInfo struct {
// VisitURL holds the URL to be visited in a web browser.
VisitURL string
// WaitTokenURL holds a URL that will block on GET
// until the browser interaction has completed.
// On success, the response is expected to hold a waitTokenResponse
// in its body holding the token to be returned from the
// Interact method.
WaitTokenURL string
}
var (
_ Interactor = WebBrowserInteractor{}
_ LegacyInteractor = WebBrowserInteractor{}
)
// OpenWebBrowser opens a web browser at the
// given URL. If the OS is not recognised, the URL
// is just printed to standard output.
func OpenWebBrowser(url *url.URL) error {
err := webbrowser.Open(url)
if err == nil {
fmt.Fprintf(os.Stderr, "Opening an authorization web page in your browser.\n")
fmt.Fprintf(os.Stderr, "If it does not open, please open this URL:\n%s\n", url)
return nil
}
if err == webbrowser.ErrNoBrowser {
fmt.Fprintf(os.Stderr, "Please open this URL in your browser to authorize:\n%s\n", url)
return nil
}
return err
}
// SetWebBrowserInteraction adds information about web-browser-based
// interaction to the given error, which should be an
// interaction-required error that's about to be returned from a
// discharge request.
//
// The visitURL parameter holds a URL that should be visited by the user
// in a web browser; the waitTokenURL parameter holds a URL that can be
// long-polled to acquire the resulting discharge token.
//
// Use SetLegacyInteraction to add support for legacy clients
// that don't understand the newer InteractionMethods field.
func SetWebBrowserInteraction(e *Error, visitURL, waitTokenURL string) {
e.SetInteraction(WebBrowserInteractionKind, WebBrowserInteractionInfo{
VisitURL: visitURL,
WaitTokenURL: waitTokenURL,
})
}
// SetLegacyInteraction adds information about web-browser-based
// interaction (or other kinds of legacy-protocol interaction) to the
// given error, which should be an interaction-required error that's
// about to be returned from a discharge request.
//
// The visitURL parameter holds a URL that should be visited by the user
// in a web browser (or with an "Accept: application/json" header to
// find out the set of legacy interaction methods).
//
// The waitURL parameter holds a URL that can be long-polled
// to acquire the discharge macaroon.
func SetLegacyInteraction(e *Error, visitURL, waitURL string) {
if e.Info == nil {
e.Info = new(ErrorInfo)
}
e.Info.LegacyVisitURL = visitURL
e.Info.LegacyWaitURL = waitURL
}
// WebBrowserInteractor handls web-browser-based
// interaction-required errors by opening a web
// browser to allow the user to prove their
// credentials interactively.
//
// It implements the Interactor interface, so instances
// can be used with Client.AddInteractor.
type WebBrowserInteractor struct {
// OpenWebBrowser is used to visit a page in
// the user's web browser. If it's nil, the
// OpenWebBrowser function will be used.
OpenWebBrowser func(*url.URL) error
}
// Kind implements Interactor.Kind.
func (WebBrowserInteractor) Kind() string {
return WebBrowserInteractionKind
}
// Interact implements Interactor.Interact by opening a new web page.
func (wi WebBrowserInteractor) Interact(ctx context.Context, client *Client, location string, irErr *Error) (*DischargeToken, error) {
var p WebBrowserInteractionInfo
if err := irErr.InteractionMethod(wi.Kind(), &p); err != nil {
return nil, errgo.Mask(err, errgo.Is(ErrInteractionMethodNotFound))
}
visitURL, err := relativeURL(location, p.VisitURL)
if err != nil {
return nil, errgo.Notef(err, "cannot make relative visit URL")
}
waitTokenURL, err := relativeURL(location, p.WaitTokenURL)
if err != nil {
return nil, errgo.Notef(err, "cannot make relative wait URL")
}
if err := wi.openWebBrowser(visitURL); err != nil {
return nil, errgo.Mask(err)
}
return waitForToken(ctx, client, waitTokenURL)
}
func (wi WebBrowserInteractor) openWebBrowser(u *url.URL) error {
open := wi.OpenWebBrowser
if open == nil {
open = OpenWebBrowser
}
if err := open(u); err != nil {
return errgo.Mask(err)
}
return nil
}
// waitForToken returns a token from a the waitToken URL
func waitForToken(ctx context.Context, client *Client, waitTokenURL *url.URL) (*DischargeToken, error) {
// TODO integrate this with waitForMacaroon somehow?
req, err := http.NewRequest("GET", waitTokenURL.String(), nil)
if err != nil {
return nil, errgo.Mask(err)
}
req = req.WithContext(ctx)
httpResp, err := client.Client.Do(req)
if err != nil {
return nil, errgo.Notef(err, "cannot get %q", waitTokenURL)
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK {
err := unmarshalError(httpResp)
return nil, errgo.NoteMask(err, "cannot acquire discharge token", errgo.Any)
}
var resp WaitTokenResponse
if err := httprequest.UnmarshalJSONResponse(httpResp, &resp); err != nil {
return nil, errgo.Notef(err, "cannot unmarshal wait response")
}
tokenVal, err := maybeBase64Decode(resp.Token, resp.Token64)
if err != nil {
return nil, errgo.Notef(err, "bad discharge token")
}
// TODO check that kind and value are non-empty?
return &DischargeToken{
Kind: resp.Kind,
Value: tokenVal,
}, nil
}
// LegacyInteract implements LegacyInteractor by opening a web browser page.
func (wi WebBrowserInteractor) LegacyInteract(ctx context.Context, client *Client, location string, visitURL *url.URL) error {
if err := wi.openWebBrowser(visitURL); err != nil {
return errgo.Mask(err)
}
return nil
}

View file

@ -1,157 +0,0 @@
package httpbakery
import (
"context"
"net"
"net/http"
"gopkg.in/errgo.v1"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
)
type httpRequestKey struct{}
// ContextWithRequest returns the context with information from the
// given request attached as context. This is used by the httpbakery
// checkers (see RegisterCheckers for details).
func ContextWithRequest(ctx context.Context, req *http.Request) context.Context {
return context.WithValue(ctx, httpRequestKey{}, req)
}
func requestFromContext(ctx context.Context) *http.Request {
req, _ := ctx.Value(httpRequestKey{}).(*http.Request)
return req
}
const (
// CondClientIPAddr holds the first party caveat condition
// that checks a client's IP address.
CondClientIPAddr = "client-ip-addr"
// CondClientOrigin holds the first party caveat condition that
// checks a client's origin header.
CondClientOrigin = "origin"
)
// CheckersNamespace holds the URI of the HTTP checkers schema.
const CheckersNamespace = "http"
var allCheckers = map[string]checkers.Func{
CondClientIPAddr: ipAddrCheck,
CondClientOrigin: clientOriginCheck,
}
// RegisterCheckers registers all the HTTP checkers with the given checker.
// Current checkers include:
//
// client-ip-addr <ip-address>
//
// The client-ip-addr caveat checks that the HTTP request has
// the given remote IP address.
//
// origin <name>
//
// The origin caveat checks that the HTTP Origin header has
// the given value.
func RegisterCheckers(c *checkers.Checker) {
c.Namespace().Register(CheckersNamespace, "http")
for cond, check := range allCheckers {
c.Register(cond, CheckersNamespace, check)
}
}
// NewChecker returns a new checker with the standard
// and HTTP checkers registered in it.
func NewChecker() *checkers.Checker {
c := checkers.New(nil)
RegisterCheckers(c)
return c
}
// ipAddrCheck implements the IP client address checker
// for an HTTP request.
func ipAddrCheck(ctx context.Context, cond, args string) error {
req := requestFromContext(ctx)
if req == nil {
return errgo.Newf("no IP address found in context")
}
ip := net.ParseIP(args)
if ip == nil {
return errgo.Newf("cannot parse IP address in caveat")
}
if req.RemoteAddr == "" {
return errgo.Newf("client has no remote address")
}
reqIP, err := requestIPAddr(req)
if err != nil {
return errgo.Mask(err)
}
if !reqIP.Equal(ip) {
return errgo.Newf("client IP address mismatch, got %s", reqIP)
}
return nil
}
// clientOriginCheck implements the Origin header checker
// for an HTTP request.
func clientOriginCheck(ctx context.Context, cond, args string) error {
req := requestFromContext(ctx)
if req == nil {
return errgo.Newf("no origin found in context")
}
// Note that web browsers may not provide the origin header when it's
// not a cross-site request with a GET method. There's nothing we
// can do about that, so just allow all requests with an empty origin.
if reqOrigin := req.Header.Get("Origin"); reqOrigin != "" && reqOrigin != args {
return errgo.Newf("request has invalid Origin header; got %q", reqOrigin)
}
return nil
}
// SameClientIPAddrCaveat returns a caveat that will check that
// the remote IP address is the same as that in the given HTTP request.
func SameClientIPAddrCaveat(req *http.Request) checkers.Caveat {
if req.RemoteAddr == "" {
return checkers.ErrorCaveatf("client has no remote IP address")
}
ip, err := requestIPAddr(req)
if err != nil {
return checkers.ErrorCaveatf("%v", err)
}
return ClientIPAddrCaveat(ip)
}
// ClientIPAddrCaveat returns a caveat that will check whether the
// client's IP address is as provided.
func ClientIPAddrCaveat(addr net.IP) checkers.Caveat {
if len(addr) != net.IPv4len && len(addr) != net.IPv6len {
return checkers.ErrorCaveatf("bad IP address %d", []byte(addr))
}
return httpCaveat(CondClientIPAddr, addr.String())
}
// ClientOriginCaveat returns a caveat that will check whether the
// client's Origin header in its HTTP request is as provided.
func ClientOriginCaveat(origin string) checkers.Caveat {
return httpCaveat(CondClientOrigin, origin)
}
func httpCaveat(cond, arg string) checkers.Caveat {
return checkers.Caveat{
Condition: checkers.Condition(cond, arg),
Namespace: CheckersNamespace,
}
}
func requestIPAddr(req *http.Request) (net.IP, error) {
reqHost, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
return nil, errgo.Newf("cannot parse host port in remote address: %v", err)
}
ip := net.ParseIP(reqHost)
if ip == nil {
return nil, errgo.Newf("invalid IP address in remote address %q", req.RemoteAddr)
}
return ip, nil
}

View file

@ -1,727 +0,0 @@
package httpbakery
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"time"
"golang.org/x/net/publicsuffix"
"gopkg.in/errgo.v1"
"gopkg.in/httprequest.v1"
"gopkg.in/macaroon.v2"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
)
var unmarshalError = httprequest.ErrorUnmarshaler(&Error{})
// maxDischargeRetries holds the maximum number of times that an HTTP
// request will be retried after a third party caveat has been successfully
// discharged.
const maxDischargeRetries = 3
// DischargeError represents the error when a third party discharge
// is refused by a server.
type DischargeError struct {
// Reason holds the underlying remote error that caused the
// discharge to fail.
Reason *Error
}
func (e *DischargeError) Error() string {
return fmt.Sprintf("third party refused discharge: %v", e.Reason)
}
// IsDischargeError reports whether err is a *DischargeError.
func IsDischargeError(err error) bool {
_, ok := err.(*DischargeError)
return ok
}
// InteractionError wraps an error returned by a call to visitWebPage.
type InteractionError struct {
// Reason holds the actual error returned from visitWebPage.
Reason error
}
func (e *InteractionError) Error() string {
return fmt.Sprintf("cannot start interactive session: %v", e.Reason)
}
// IsInteractionError reports whether err is an *InteractionError.
func IsInteractionError(err error) bool {
_, ok := err.(*InteractionError)
return ok
}
// NewHTTPClient returns an http.Client that ensures
// that headers are sent to the server even when the
// server redirects a GET request. The returned client
// also contains an empty in-memory cookie jar.
//
// See https://github.com/golang/go/issues/4677
func NewHTTPClient() *http.Client {
c := *http.DefaultClient
c.CheckRedirect = func(req *http.Request, via []*http.Request) error {
if len(via) >= 10 {
return fmt.Errorf("too many redirects")
}
if len(via) == 0 {
return nil
}
for attr, val := range via[0].Header {
if attr == "Cookie" {
// Cookies are added automatically anyway.
continue
}
if _, ok := req.Header[attr]; !ok {
req.Header[attr] = val
}
}
return nil
}
jar, err := cookiejar.New(&cookiejar.Options{
PublicSuffixList: publicsuffix.List,
})
if err != nil {
panic(err)
}
c.Jar = jar
return &c
}
// Client holds the context for making HTTP requests
// that automatically acquire and discharge macaroons.
type Client struct {
// Client holds the HTTP client to use. It should have a cookie
// jar configured, and when redirecting it should preserve the
// headers (see NewHTTPClient).
*http.Client
// InteractionMethods holds a slice of supported interaction
// methods, with preferred methods earlier in the slice.
// On receiving an interaction-required error when discharging,
// the Kind method of each Interactor in turn will be called
// and, if the error indicates that the interaction kind is supported,
// the Interact method will be called to complete the discharge.
InteractionMethods []Interactor
// Key holds the client's key. If set, the client will try to
// discharge third party caveats with the special location
// "local" by using this key. See bakery.DischargeAllWithKey and
// bakery.LocalThirdPartyCaveat for more information
Key *bakery.KeyPair
// Logger is used to log information about client activities.
// If it is nil, bakery.DefaultLogger("httpbakery") will be used.
Logger bakery.Logger
}
// An Interactor represents a way of persuading a discharger
// that it should grant a discharge macaroon.
type Interactor interface {
// Kind returns the interaction method name. This corresponds to the
// key in the Error.InteractionMethods type.
Kind() string
// Interact performs the interaction, and returns a token that can be
// used to acquire the discharge macaroon. The location provides
// the third party caveat location to make it possible to use
// relative URLs.
//
// If the given interaction isn't supported by the client for
// the given location, it may return an error with an
// ErrInteractionMethodNotFound cause which will cause the
// interactor to be ignored that time.
Interact(ctx context.Context, client *Client, location string, interactionRequiredErr *Error) (*DischargeToken, error)
}
// DischargeToken holds a token that is intended
// to persuade a discharger to discharge a third
// party caveat.
type DischargeToken struct {
// Kind holds the kind of the token. By convention this
// matches the name of the interaction method used to
// obtain the token, but that's not required.
Kind string `json:"kind"`
// Value holds the value of the token.
Value []byte `json:"value"`
}
// LegacyInteractor may optionally be implemented by Interactor
// implementations that implement the legacy interaction-required
// error protocols.
type LegacyInteractor interface {
// LegacyInteract implements the "visit" half of a legacy discharge
// interaction. The "wait" half will be implemented by httpbakery.
// The location is the location specified by the third party
// caveat.
LegacyInteract(ctx context.Context, client *Client, location string, visitURL *url.URL) error
}
// NewClient returns a new Client containing an HTTP client
// created with NewHTTPClient and leaves all other fields zero.
func NewClient() *Client {
return &Client{
Client: NewHTTPClient(),
}
}
// AddInteractor is a convenience method that appends the given
// interactor to c.InteractionMethods.
// For example, to enable web-browser interaction on
// a client c, do:
//
// c.AddInteractor(httpbakery.WebBrowserWindowInteractor)
func (c *Client) AddInteractor(i Interactor) {
c.InteractionMethods = append(c.InteractionMethods, i)
}
// DischargeAll attempts to acquire discharge macaroons for all the
// third party caveats in m, and returns a slice containing all
// of them bound to m.
//
// If the discharge fails because a third party refuses to discharge a
// caveat, the returned error will have a cause of type *DischargeError.
// If the discharge fails because visitWebPage returns an error,
// the returned error will have a cause of *InteractionError.
//
// The returned macaroon slice will not be stored in the client
// cookie jar (see SetCookie if you need to do that).
func (c *Client) DischargeAll(ctx context.Context, m *bakery.Macaroon) (macaroon.Slice, error) {
return bakery.DischargeAllWithKey(ctx, m, c.AcquireDischarge, c.Key)
}
// DischargeAllUnbound is like DischargeAll except that it does not
// bind the resulting macaroons.
func (c *Client) DischargeAllUnbound(ctx context.Context, ms bakery.Slice) (bakery.Slice, error) {
return ms.DischargeAll(ctx, c.AcquireDischarge, c.Key)
}
// Do is like DoWithContext, except the context is automatically derived.
// If using go version 1.7 or later the context will be taken from the
// given request, otherwise context.Background() will be used.
func (c *Client) Do(req *http.Request) (*http.Response, error) {
return c.do(contextFromRequest(req), req, nil)
}
// DoWithContext sends the given HTTP request and returns its response.
// If the request fails with a discharge-required error, any required
// discharge macaroons will be acquired, and the request will be repeated
// with those attached.
//
// If the required discharges were refused by a third party, an error
// with a *DischargeError cause will be returned.
//
// If interaction is required by the user, the client's InteractionMethods
// will be used to perform interaction. An error
// with a *InteractionError cause will be returned if this interaction
// fails. See WebBrowserWindowInteractor for a possible implementation of
// an Interactor for an interaction method.
//
// DoWithContext may add headers to req.Header.
func (c *Client) DoWithContext(ctx context.Context, req *http.Request) (*http.Response, error) {
return c.do(ctx, req, nil)
}
// DoWithCustomError is like Do except it allows a client
// to specify a custom error function, getError, which is called on the
// HTTP response and may return a non-nil error if the response holds an
// error. If the cause of the returned error is a *Error value and its
// code is ErrDischargeRequired, the macaroon in its Info field will be
// discharged and the request will be repeated with the discharged
// macaroon. If getError returns nil, it should leave the response body
// unchanged.
//
// If getError is nil, DefaultGetError will be used.
//
// This method can be useful when dealing with APIs that
// return their errors in a format incompatible with Error, but the
// need for it should be avoided when creating new APIs,
// as it makes the endpoints less amenable to generic tools.
func (c *Client) DoWithCustomError(req *http.Request, getError func(resp *http.Response) error) (*http.Response, error) {
return c.do(contextFromRequest(req), req, getError)
}
func (c *Client) do(ctx context.Context, req *http.Request, getError func(resp *http.Response) error) (*http.Response, error) {
c.logDebugf(ctx, "client do %s %s {", req.Method, req.URL)
resp, err := c.do1(ctx, req, getError)
c.logDebugf(ctx, "} -> error %#v", err)
return resp, err
}
func (c *Client) do1(ctx context.Context, req *http.Request, getError func(resp *http.Response) error) (*http.Response, error) {
if getError == nil {
getError = DefaultGetError
}
if c.Client.Jar == nil {
return nil, errgo.New("no cookie jar supplied in HTTP client")
}
rreq, ok := newRetryableRequest(c.Client, req)
if !ok {
return nil, fmt.Errorf("request body is not seekable")
}
defer rreq.close()
req.Header.Set(BakeryProtocolHeader, fmt.Sprint(bakery.LatestVersion))
// Make several attempts to do the request, because we might have
// to get through several layers of security. We only retry if
// we get a DischargeRequiredError and succeed in discharging
// the macaroon in it.
retry := 0
for {
resp, err := c.do2(ctx, rreq, getError)
if err == nil || !isDischargeRequiredError(err) {
return resp, errgo.Mask(err, errgo.Any)
}
if retry++; retry > maxDischargeRetries {
return nil, errgo.NoteMask(err, fmt.Sprintf("too many (%d) discharge requests", retry-1), errgo.Any)
}
if err1 := c.HandleError(ctx, req.URL, err); err1 != nil {
return nil, errgo.Mask(err1, errgo.Any)
}
c.logDebugf(ctx, "discharge succeeded; retry %d", retry)
}
}
func (c *Client) do2(ctx context.Context, rreq *retryableRequest, getError func(resp *http.Response) error) (*http.Response, error) {
httpResp, err := rreq.do(ctx)
if err != nil {
return nil, errgo.Mask(err, errgo.Any)
}
err = getError(httpResp)
if err == nil {
c.logInfof(ctx, "HTTP response OK (status %v)", httpResp.Status)
return httpResp, nil
}
httpResp.Body.Close()
return nil, errgo.Mask(err, errgo.Any)
}
// HandleError tries to resolve the given error, which should be a
// response to the given URL, by discharging any macaroon contained in
// it. That is, if the error cause is an *Error and its code is
// ErrDischargeRequired, then it will try to discharge
// err.Info.Macaroon. If the discharge succeeds, the discharged macaroon
// will be saved to the client's cookie jar and ResolveError will return
// nil.
//
// For any other kind of error, the original error will be returned.
func (c *Client) HandleError(ctx context.Context, reqURL *url.URL, err error) error {
respErr, ok := errgo.Cause(err).(*Error)
if !ok {
return err
}
if respErr.Code != ErrDischargeRequired {
return respErr
}
if respErr.Info == nil || respErr.Info.Macaroon == nil {
return errgo.New("no macaroon found in discharge-required response")
}
mac := respErr.Info.Macaroon
macaroons, err := bakery.DischargeAllWithKey(ctx, mac, c.AcquireDischarge, c.Key)
if err != nil {
return errgo.Mask(err, errgo.Any)
}
var cookiePath string
if path := respErr.Info.MacaroonPath; path != "" {
relURL, err := parseURLPath(path)
if err != nil {
c.logInfof(ctx, "ignoring invalid path in discharge-required response: %v", err)
} else {
cookiePath = reqURL.ResolveReference(relURL).Path
}
}
// TODO use a namespace taken from the error response.
cookie, err := NewCookie(nil, macaroons)
if err != nil {
return errgo.Notef(err, "cannot make cookie")
}
cookie.Path = cookiePath
if name := respErr.Info.CookieNameSuffix; name != "" {
cookie.Name = "macaroon-" + name
}
c.Jar.SetCookies(reqURL, []*http.Cookie{cookie})
return nil
}
// DefaultGetError is the default error unmarshaler used by Client.Do.
func DefaultGetError(httpResp *http.Response) error {
if httpResp.StatusCode != http.StatusProxyAuthRequired && httpResp.StatusCode != http.StatusUnauthorized {
return nil
}
// Check for the new protocol discharge error.
if httpResp.StatusCode == http.StatusUnauthorized && httpResp.Header.Get("WWW-Authenticate") != "Macaroon" {
return nil
}
if httpResp.Header.Get("Content-Type") != "application/json" {
return nil
}
var resp Error
if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
return fmt.Errorf("cannot unmarshal error response: %v", err)
}
return &resp
}
func parseURLPath(path string) (*url.URL, error) {
u, err := url.Parse(path)
if err != nil {
return nil, errgo.Mask(err)
}
if u.Scheme != "" ||
u.Opaque != "" ||
u.User != nil ||
u.Host != "" ||
u.RawQuery != "" ||
u.Fragment != "" {
return nil, errgo.Newf("URL path %q is not clean", path)
}
return u, nil
}
// PermanentExpiryDuration holds the length of time a cookie
// holding a macaroon with no time-before caveat will be
// stored.
const PermanentExpiryDuration = 100 * 365 * 24 * time.Hour
// NewCookie takes a slice of macaroons and returns them
// encoded as a cookie. The slice should contain a single primary
// macaroon in its first element, and any discharges after that.
//
// The given namespace specifies the first party caveat namespace,
// used for deriving the expiry time of the cookie.
func NewCookie(ns *checkers.Namespace, ms macaroon.Slice) (*http.Cookie, error) {
if len(ms) == 0 {
return nil, errgo.New("no macaroons in cookie")
}
// TODO(rog) marshal cookie as binary if version allows.
data, err := json.Marshal(ms)
if err != nil {
return nil, errgo.Notef(err, "cannot marshal macaroons")
}
cookie := &http.Cookie{
Name: fmt.Sprintf("macaroon-%x", ms[0].Signature()),
Value: base64.StdEncoding.EncodeToString(data),
}
expires, found := checkers.MacaroonsExpiryTime(ns, ms)
if !found {
// The macaroon doesn't expire - use a very long expiry
// time for the cookie.
expires = time.Now().Add(PermanentExpiryDuration)
} else if expires.Sub(time.Now()) < time.Minute {
// The macaroon might have expired already, or it's
// got a short duration, so treat it as a session cookie
// by setting Expires to the zero time.
expires = time.Time{}
}
cookie.Expires = expires
// TODO(rog) other fields.
return cookie, nil
}
// SetCookie sets a cookie for the given URL on the given cookie jar
// that will holds the given macaroon slice. The macaroon slice should
// contain a single primary macaroon in its first element, and any
// discharges after that.
//
// The given namespace specifies the first party caveat namespace,
// used for deriving the expiry time of the cookie.
func SetCookie(jar http.CookieJar, url *url.URL, ns *checkers.Namespace, ms macaroon.Slice) error {
cookie, err := NewCookie(ns, ms)
if err != nil {
return errgo.Mask(err)
}
jar.SetCookies(url, []*http.Cookie{cookie})
return nil
}
// MacaroonsForURL returns any macaroons associated with the
// given URL in the given cookie jar.
func MacaroonsForURL(jar http.CookieJar, u *url.URL) []macaroon.Slice {
return cookiesToMacaroons(jar.Cookies(u))
}
func appendURLElem(u, elem string) string {
if strings.HasSuffix(u, "/") {
return u + elem
}
return u + "/" + elem
}
// AcquireDischarge acquires a discharge macaroon from the caveat location as an HTTP URL.
// It fits the getDischarge argument type required by bakery.DischargeAll.
func (c *Client) AcquireDischarge(ctx context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) {
m, err := c.acquireDischarge(ctx, cav, payload, nil)
if err == nil {
return m, nil
}
cause, ok := errgo.Cause(err).(*Error)
if !ok {
return nil, errgo.NoteMask(err, "cannot acquire discharge", IsInteractionError)
}
if cause.Code != ErrInteractionRequired {
return nil, &DischargeError{
Reason: cause,
}
}
if cause.Info == nil {
return nil, errgo.Notef(err, "interaction-required response with no info")
}
// Make sure the location has a trailing slash so that
// the relative URL calculations work correctly even when
// cav.Location doesn't have a trailing slash.
loc := appendURLElem(cav.Location, "")
token, m, err := c.interact(ctx, loc, cause, payload)
if err != nil {
return nil, errgo.Mask(err, IsDischargeError, IsInteractionError)
}
if m != nil {
// We've acquired the macaroon directly via legacy interaction.
return m, nil
}
// Try to acquire the discharge again, but this time with
// the token acquired by the interaction method.
m, err = c.acquireDischarge(ctx, cav, payload, token)
if err != nil {
return nil, errgo.Mask(err, IsDischargeError, IsInteractionError)
}
return m, nil
}
// acquireDischarge is like AcquireDischarge except that it also
// takes a token acquired from an interaction method.
func (c *Client) acquireDischarge(
ctx context.Context,
cav macaroon.Caveat,
payload []byte,
token *DischargeToken,
) (*bakery.Macaroon, error) {
dclient := newDischargeClient(cav.Location, c)
var req dischargeRequest
req.Id, req.Id64 = maybeBase64Encode(cav.Id)
if token != nil {
req.Token, req.Token64 = maybeBase64Encode(token.Value)
req.TokenKind = token.Kind
}
req.Caveat = base64.RawURLEncoding.EncodeToString(payload)
resp, err := dclient.Discharge(ctx, &req)
if err == nil {
return resp.Macaroon, nil
}
return nil, errgo.Mask(err, errgo.Any)
}
// interact gathers a macaroon by directing the user to interact with a
// web page. The irErr argument holds the interaction-required
// error response.
func (c *Client) interact(ctx context.Context, location string, irErr *Error, payload []byte) (*DischargeToken, *bakery.Macaroon, error) {
if len(c.InteractionMethods) == 0 {
return nil, nil, &InteractionError{
Reason: errgo.New("interaction required but not possible"),
}
}
if irErr.Info.InteractionMethods == nil && irErr.Info.LegacyVisitURL != "" {
// It's an old-style error; deal with it differently.
m, err := c.legacyInteract(ctx, location, irErr)
if err != nil {
return nil, nil, errgo.Mask(err, IsDischargeError, IsInteractionError)
}
return nil, m, nil
}
for _, interactor := range c.InteractionMethods {
c.logDebugf(ctx, "checking interaction method %q", interactor.Kind())
if _, ok := irErr.Info.InteractionMethods[interactor.Kind()]; ok {
c.logDebugf(ctx, "found possible interaction method %q", interactor.Kind())
token, err := interactor.Interact(ctx, c, location, irErr)
if err != nil {
if errgo.Cause(err) == ErrInteractionMethodNotFound {
continue
}
return nil, nil, errgo.Mask(err, IsDischargeError, IsInteractionError)
}
if token == nil {
return nil, nil, errgo.New("interaction method returned an empty token")
}
return token, nil, nil
} else {
c.logDebugf(ctx, "interaction method %q not found in %#v", interactor.Kind(), irErr.Info.InteractionMethods)
}
}
return nil, nil, &InteractionError{
Reason: errgo.Newf("no supported interaction method"),
}
}
func (c *Client) legacyInteract(ctx context.Context, location string, irErr *Error) (*bakery.Macaroon, error) {
visitURL, err := relativeURL(location, irErr.Info.LegacyVisitURL)
if err != nil {
return nil, errgo.Mask(err)
}
waitURL, err := relativeURL(location, irErr.Info.LegacyWaitURL)
if err != nil {
return nil, errgo.Mask(err)
}
methodURLs := map[string]*url.URL{
"interactive": visitURL,
}
if len(c.InteractionMethods) > 1 || c.InteractionMethods[0].Kind() != WebBrowserInteractionKind {
// We have several possible methods or we only support a non-window
// method, so we need to fetch the possible methods supported by the discharger.
methodURLs = legacyGetInteractionMethods(ctx, c.logger(), c, visitURL)
}
for _, interactor := range c.InteractionMethods {
kind := interactor.Kind()
if kind == WebBrowserInteractionKind {
// This is the old name for browser-window interaction.
kind = "interactive"
}
interactor, ok := interactor.(LegacyInteractor)
if !ok {
// Legacy interaction mode isn't supported.
continue
}
visitURL, ok := methodURLs[kind]
if !ok {
continue
}
visitURL, err := relativeURL(location, visitURL.String())
if err != nil {
return nil, errgo.Mask(err)
}
if err := interactor.LegacyInteract(ctx, c, location, visitURL); err != nil {
return nil, &InteractionError{
Reason: errgo.Mask(err, errgo.Any),
}
}
return waitForMacaroon(ctx, c, waitURL)
}
return nil, &InteractionError{
Reason: errgo.Newf("no methods supported"),
}
}
func (c *Client) logDebugf(ctx context.Context, f string, a ...interface{}) {
c.logger().Debugf(ctx, f, a...)
}
func (c *Client) logInfof(ctx context.Context, f string, a ...interface{}) {
c.logger().Infof(ctx, f, a...)
}
func (c *Client) logger() bakery.Logger {
if c.Logger != nil {
return c.Logger
}
return bakery.DefaultLogger("httpbakery")
}
// waitForMacaroon returns a macaroon from a legacy wait endpoint.
func waitForMacaroon(ctx context.Context, client *Client, waitURL *url.URL) (*bakery.Macaroon, error) {
req, err := http.NewRequest("GET", waitURL.String(), nil)
if err != nil {
return nil, errgo.Mask(err)
}
req = req.WithContext(ctx)
httpResp, err := client.Client.Do(req)
if err != nil {
return nil, errgo.Notef(err, "cannot get %q", waitURL)
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK {
err := unmarshalError(httpResp)
if err1, ok := err.(*Error); ok {
err = &DischargeError{
Reason: err1,
}
}
return nil, errgo.NoteMask(err, "failed to acquire macaroon after waiting", errgo.Any)
}
var resp WaitResponse
if err := httprequest.UnmarshalJSONResponse(httpResp, &resp); err != nil {
return nil, errgo.Notef(err, "cannot unmarshal wait response")
}
return resp.Macaroon, nil
}
// relativeURL returns newPath relative to an original URL.
func relativeURL(base, new string) (*url.URL, error) {
if new == "" {
return nil, errgo.Newf("empty URL")
}
baseURL, err := url.Parse(base)
if err != nil {
return nil, errgo.Notef(err, "cannot parse URL")
}
newURL, err := url.Parse(new)
if err != nil {
return nil, errgo.Notef(err, "cannot parse URL")
}
return baseURL.ResolveReference(newURL), nil
}
// TODO(rog) move a lot of the code below into server.go, as it's
// much more about server side than client side.
// MacaroonsHeader is the key of the HTTP header that can be used to provide a
// macaroon for request authorization.
const MacaroonsHeader = "Macaroons"
// RequestMacaroons returns any collections of macaroons from the header and
// cookies found in the request. By convention, each slice will contain a
// primary macaroon followed by its discharges.
func RequestMacaroons(req *http.Request) []macaroon.Slice {
mss := cookiesToMacaroons(req.Cookies())
for _, h := range req.Header[MacaroonsHeader] {
ms, err := decodeMacaroonSlice(h)
if err != nil {
// Ignore invalid macaroons.
continue
}
mss = append(mss, ms)
}
return mss
}
// cookiesToMacaroons returns a slice of any macaroons found
// in the given slice of cookies.
func cookiesToMacaroons(cookies []*http.Cookie) []macaroon.Slice {
var mss []macaroon.Slice
for _, cookie := range cookies {
if !strings.HasPrefix(cookie.Name, "macaroon-") {
continue
}
ms, err := decodeMacaroonSlice(cookie.Value)
if err != nil {
// Ignore invalid macaroons.
continue
}
mss = append(mss, ms)
}
return mss
}
// decodeMacaroonSlice decodes a base64-JSON-encoded slice of macaroons from
// the given string.
func decodeMacaroonSlice(value string) (macaroon.Slice, error) {
data, err := macaroon.Base64Decode([]byte(value))
if err != nil {
return nil, errgo.NoteMask(err, "cannot base64-decode macaroons")
}
// TODO(rog) accept binary encoded macaroon cookies.
var ms macaroon.Slice
if err := json.Unmarshal(data, &ms); err != nil {
return nil, errgo.NoteMask(err, "cannot unmarshal macaroons")
}
return ms, nil
}

View file

@ -1,12 +0,0 @@
// +build go1.7
package httpbakery
import (
"context"
"net/http"
)
func contextFromRequest(req *http.Request) context.Context {
return req.Context()
}

View file

@ -1,12 +0,0 @@
// +build !go1.7
package httpbakery
import (
"context"
"net/http"
)
func contextFromRequest(req *http.Request) context.Context {
return context.Background()
}

View file

@ -1,367 +0,0 @@
package httpbakery
import (
"context"
"encoding/base64"
"net/http"
"path"
"unicode/utf8"
"github.com/julienschmidt/httprouter"
"gopkg.in/errgo.v1"
"gopkg.in/httprequest.v1"
"gopkg.in/macaroon.v2"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
)
// ThirdPartyCaveatChecker is used to check third party caveats.
// This interface is deprecated and included only for backward
// compatibility; ThirdPartyCaveatCheckerP should be used instead.
type ThirdPartyCaveatChecker interface {
// CheckThirdPartyCaveat is like ThirdPartyCaveatCheckerP.CheckThirdPartyCaveat
// except that it uses separate arguments instead of a struct arg.
CheckThirdPartyCaveat(ctx context.Context, info *bakery.ThirdPartyCaveatInfo, req *http.Request, token *DischargeToken) ([]checkers.Caveat, error)
}
// ThirdPartyCaveatCheckerP is used to check third party caveats.
// The "P" stands for "Params" - this was added after ThirdPartyCaveatChecker
// which can't be removed without breaking backwards compatibility.
type ThirdPartyCaveatCheckerP interface {
// CheckThirdPartyCaveat is used to check whether a client
// making the given request should be allowed a discharge for
// the p.Info.Condition. On success, the caveat will be discharged,
// with any returned caveats also added to the discharge
// macaroon.
//
// The p.Token field, if non-nil, is a token obtained from
// Interactor.Interact as the result of a discharge interaction
// after an interaction required error.
//
// Note than when used in the context of a discharge handler
// created by Discharger, any returned errors will be marshaled
// as documented in DischargeHandler.ErrorMapper.
CheckThirdPartyCaveat(ctx context.Context, p ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error)
}
// ThirdPartyCaveatCheckerParams holds the parameters passed to
// CheckThirdPartyCaveatP.
type ThirdPartyCaveatCheckerParams struct {
// Caveat holds information about the caveat being discharged.
Caveat *bakery.ThirdPartyCaveatInfo
// Token holds the discharge token provided by the client, if any.
Token *DischargeToken
// Req holds the HTTP discharge request.
Request *http.Request
// Response holds the HTTP response writer. Implementations
// must not call its WriteHeader or Write methods.
Response http.ResponseWriter
}
// ThirdPartyCaveatCheckerFunc implements ThirdPartyCaveatChecker
// by calling a function.
type ThirdPartyCaveatCheckerFunc func(ctx context.Context, req *http.Request, info *bakery.ThirdPartyCaveatInfo, token *DischargeToken) ([]checkers.Caveat, error)
func (f ThirdPartyCaveatCheckerFunc) CheckThirdPartyCaveat(ctx context.Context, info *bakery.ThirdPartyCaveatInfo, req *http.Request, token *DischargeToken) ([]checkers.Caveat, error) {
return f(ctx, req, info, token)
}
// ThirdPartyCaveatCheckerPFunc implements ThirdPartyCaveatCheckerP
// by calling a function.
type ThirdPartyCaveatCheckerPFunc func(ctx context.Context, p ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error)
func (f ThirdPartyCaveatCheckerPFunc) CheckThirdPartyCaveat(ctx context.Context, p ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) {
return f(ctx, p)
}
// newDischargeClient returns a discharge client that addresses the
// third party discharger at the given location URL and uses
// the given client to make HTTP requests.
//
// If client is nil, http.DefaultClient is used.
func newDischargeClient(location string, client httprequest.Doer) *dischargeClient {
if client == nil {
client = http.DefaultClient
}
return &dischargeClient{
Client: httprequest.Client{
BaseURL: location,
Doer: client,
UnmarshalError: unmarshalError,
},
}
}
// Discharger holds parameters for creating a new Discharger.
type DischargerParams struct {
// CheckerP is used to actually check the caveats.
// This will be used in preference to Checker.
CheckerP ThirdPartyCaveatCheckerP
// Checker is used to actually check the caveats.
// This should be considered deprecated and will be ignored if CheckerP is set.
Checker ThirdPartyCaveatChecker
// Key holds the key pair of the discharger.
Key *bakery.KeyPair
// Locator is used to find public keys when adding
// third-party caveats on discharge macaroons.
// If this is nil, no third party caveats may be added.
Locator bakery.ThirdPartyLocator
// ErrorToResponse is used to convert errors returned by the third
// party caveat checker to the form that will be JSON-marshaled
// on the wire. If zero, this defaults to ErrorToResponse.
// If set, it should handle errors that it does not understand
// by falling back to calling ErrorToResponse to ensure
// that the standard bakery errors are marshaled in the expected way.
ErrorToResponse func(ctx context.Context, err error) (int, interface{})
}
// Discharger represents a third-party caveat discharger.
// can discharge caveats in an HTTP server.
//
// The name space served by dischargers is as follows.
// All parameters can be provided either as URL attributes
// or form attributes. The result is always formatted as a JSON
// object.
//
// On failure, all endpoints return an error described by
// the Error type.
//
// POST /discharge
// params:
// id: all-UTF-8 third party caveat id
// id64: non-padded URL-base64 encoded caveat id
// macaroon-id: (optional) id to give to discharge macaroon (defaults to id)
// token: (optional) value of discharge token
// token64: (optional) base64-encoded value of discharge token.
// token-kind: (mandatory if token or token64 provided) discharge token kind.
// result on success (http.StatusOK):
// {
// Macaroon *macaroon.Macaroon
// }
//
// GET /publickey
// result:
// public key of service
// expiry time of key
type Discharger struct {
p DischargerParams
}
// NewDischarger returns a new third-party caveat discharger
// using the given parameters.
func NewDischarger(p DischargerParams) *Discharger {
if p.ErrorToResponse == nil {
p.ErrorToResponse = ErrorToResponse
}
if p.Locator == nil {
p.Locator = emptyLocator{}
}
if p.CheckerP == nil {
p.CheckerP = ThirdPartyCaveatCheckerPFunc(func(ctx context.Context, cp ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) {
return p.Checker.CheckThirdPartyCaveat(ctx, cp.Caveat, cp.Request, cp.Token)
})
}
return &Discharger{
p: p,
}
}
type emptyLocator struct{}
func (emptyLocator) ThirdPartyInfo(ctx context.Context, loc string) (bakery.ThirdPartyInfo, error) {
return bakery.ThirdPartyInfo{}, bakery.ErrNotFound
}
// AddMuxHandlers adds handlers to the given ServeMux to provide
// a third-party caveat discharge service.
func (d *Discharger) AddMuxHandlers(mux *http.ServeMux, rootPath string) {
for _, h := range d.Handlers() {
// Note: this only works because we don't have any wildcard
// patterns in the discharger paths.
mux.Handle(path.Join(rootPath, h.Path), mkHTTPHandler(h.Handle))
}
}
// Handlers returns a slice of handlers that can handle a third-party
// caveat discharge service when added to an httprouter.Router.
// TODO provide some way of customizing the context so that
// ErrorToResponse can see a request-specific context.
func (d *Discharger) Handlers() []httprequest.Handler {
f := func(p httprequest.Params) (dischargeHandler, context.Context, error) {
return dischargeHandler{
discharger: d,
}, p.Context, nil
}
srv := httprequest.Server{
ErrorMapper: d.p.ErrorToResponse,
}
return srv.Handlers(f)
}
//go:generate httprequest-generate-client github.com/go-macaroon-bakery/macaroon-bakery/v3-unstable/httpbakery dischargeHandler dischargeClient
// dischargeHandler is the type used to define the httprequest handler
// methods for a discharger.
type dischargeHandler struct {
discharger *Discharger
}
// dischargeRequest is a request to create a macaroon that discharges the
// supplied third-party caveat. Discharging caveats will normally be
// handled by the bakery it would be unusual to use this type directly in
// client software.
type dischargeRequest struct {
httprequest.Route `httprequest:"POST /discharge"`
Id string `httprequest:"id,form,omitempty"`
Id64 string `httprequest:"id64,form,omitempty"`
Caveat string `httprequest:"caveat64,form,omitempty"`
Token string `httprequest:"token,form,omitempty"`
Token64 string `httprequest:"token64,form,omitempty"`
TokenKind string `httprequest:"token-kind,form,omitempty"`
}
// dischargeResponse contains the response from a /discharge POST request.
type dischargeResponse struct {
Macaroon *bakery.Macaroon `json:",omitempty"`
}
// Discharge discharges a third party caveat.
func (h dischargeHandler) Discharge(p httprequest.Params, r *dischargeRequest) (*dischargeResponse, error) {
id, err := maybeBase64Decode(r.Id, r.Id64)
if err != nil {
return nil, errgo.Notef(err, "bad caveat id")
}
var caveat []byte
if r.Caveat != "" {
// Note that it's important that when r.Caveat is empty,
// we leave DischargeParams.Caveat as nil (Base64Decode
// always returns a non-nil byte slice).
caveat1, err := macaroon.Base64Decode([]byte(r.Caveat))
if err != nil {
return nil, errgo.Notef(err, "bad base64-encoded caveat: %v", err)
}
caveat = caveat1
}
tokenVal, err := maybeBase64Decode(r.Token, r.Token64)
if err != nil {
return nil, errgo.Notef(err, "bad discharge token")
}
var token *DischargeToken
if len(tokenVal) != 0 {
if r.TokenKind == "" {
return nil, errgo.Notef(err, "discharge token provided without token kind")
}
token = &DischargeToken{
Kind: r.TokenKind,
Value: tokenVal,
}
}
m, err := bakery.Discharge(p.Context, bakery.DischargeParams{
Id: id,
Caveat: caveat,
Key: h.discharger.p.Key,
Checker: bakery.ThirdPartyCaveatCheckerFunc(
func(ctx context.Context, cav *bakery.ThirdPartyCaveatInfo) ([]checkers.Caveat, error) {
return h.discharger.p.CheckerP.CheckThirdPartyCaveat(ctx, ThirdPartyCaveatCheckerParams{
Caveat: cav,
Request: p.Request,
Response: p.Response,
Token: token,
})
},
),
Locator: h.discharger.p.Locator,
})
if err != nil {
return nil, errgo.NoteMask(err, "cannot discharge", errgo.Any)
}
return &dischargeResponse{m}, nil
}
// publicKeyRequest specifies the /publickey endpoint.
type publicKeyRequest struct {
httprequest.Route `httprequest:"GET /publickey"`
}
// publicKeyResponse is the response to a /publickey GET request.
type publicKeyResponse struct {
PublicKey *bakery.PublicKey
}
// dischargeInfoRequest specifies the /discharge/info endpoint.
type dischargeInfoRequest struct {
httprequest.Route `httprequest:"GET /discharge/info"`
}
// dischargeInfoResponse is the response to a /discharge/info GET
// request.
type dischargeInfoResponse struct {
PublicKey *bakery.PublicKey
Version bakery.Version
}
// PublicKey returns the public key of the discharge service.
func (h dischargeHandler) PublicKey(*publicKeyRequest) (publicKeyResponse, error) {
return publicKeyResponse{
PublicKey: &h.discharger.p.Key.Public,
}, nil
}
// DischargeInfo returns information on the discharger.
func (h dischargeHandler) DischargeInfo(*dischargeInfoRequest) (dischargeInfoResponse, error) {
return dischargeInfoResponse{
PublicKey: &h.discharger.p.Key.Public,
Version: bakery.LatestVersion,
}, nil
}
// mkHTTPHandler converts an httprouter handler to an http.Handler,
// assuming that the httprouter handler has no wildcard path
// parameters.
func mkHTTPHandler(h httprouter.Handle) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
h(w, req, nil)
})
}
// maybeBase64Encode encodes b as is if it's
// OK to be passed as a URL form parameter,
// or encoded as base64 otherwise.
func maybeBase64Encode(b []byte) (s, s64 string) {
if utf8.Valid(b) {
valid := true
for _, c := range b {
if c < 32 || c == 127 {
valid = false
break
}
}
if valid {
return string(b), ""
}
}
return "", base64.RawURLEncoding.EncodeToString(b)
}
// maybeBase64Decode implements the inverse of maybeBase64Encode.
func maybeBase64Decode(s, s64 string) ([]byte, error) {
if s64 != "" {
data, err := macaroon.Base64Decode([]byte(s64))
if err != nil {
return nil, errgo.Mask(err)
}
if len(data) == 0 {
return nil, nil
}
return data, nil
}
return []byte(s), nil
}

View file

@ -1,35 +0,0 @@
// The code in this file was automatically generated by running httprequest-generate-client.
// DO NOT EDIT
package httpbakery
import (
"context"
"gopkg.in/httprequest.v1"
)
type dischargeClient struct {
Client httprequest.Client
}
// Discharge discharges a third party caveat.
func (c *dischargeClient) Discharge(ctx context.Context, p *dischargeRequest) (*dischargeResponse, error) {
var r *dischargeResponse
err := c.Client.Call(ctx, p, &r)
return r, err
}
// DischargeInfo returns information on the discharger.
func (c *dischargeClient) DischargeInfo(ctx context.Context, p *dischargeInfoRequest) (dischargeInfoResponse, error) {
var r dischargeInfoResponse
err := c.Client.Call(ctx, p, &r)
return r, err
}
// PublicKey returns the public key of the discharge service.
func (c *dischargeClient) PublicKey(ctx context.Context, p *publicKeyRequest) (publicKeyResponse, error) {
var r publicKeyResponse
err := c.Client.Call(ctx, p, &r)
return r, err
}

View file

@ -1,359 +0,0 @@
package httpbakery
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"gopkg.in/errgo.v1"
"gopkg.in/httprequest.v1"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/internal/httputil"
)
// ErrorCode holds an error code that classifies
// an error returned from a bakery HTTP handler.
type ErrorCode string
func (e ErrorCode) Error() string {
return string(e)
}
func (e ErrorCode) ErrorCode() ErrorCode {
return e
}
const (
ErrBadRequest = ErrorCode("bad request")
ErrDischargeRequired = ErrorCode("macaroon discharge required")
ErrInteractionRequired = ErrorCode("interaction required")
ErrInteractionMethodNotFound = ErrorCode("discharger does not provide an supported interaction method")
ErrPermissionDenied = ErrorCode("permission denied")
)
var httpReqServer = httprequest.Server{
ErrorMapper: ErrorToResponse,
}
// WriteError writes the given bakery error to w.
func WriteError(ctx context.Context, w http.ResponseWriter, err error) {
httpReqServer.WriteError(ctx, w, err)
}
// Error holds the type of a response from an httpbakery HTTP request,
// marshaled as JSON.
//
// Note: Do not construct Error values with ErrDischargeRequired or
// ErrInteractionRequired codes directly - use the
// NewDischargeRequiredError or NewInteractionRequiredError
// functions instead.
type Error struct {
Code ErrorCode `json:",omitempty"`
Message string `json:",omitempty"`
Info *ErrorInfo `json:",omitempty"`
// version holds the protocol version that was used
// to create the error (see NewDischargeRequiredError).
version bakery.Version
}
// ErrorInfo holds additional information provided
// by an error.
type ErrorInfo struct {
// Macaroon may hold a macaroon that, when
// discharged, may allow access to a service.
// This field is associated with the ErrDischargeRequired
// error code.
Macaroon *bakery.Macaroon `json:",omitempty"`
// MacaroonPath holds the URL path to be associated
// with the macaroon. The macaroon is potentially
// valid for all URLs under the given path.
// If it is empty, the macaroon will be associated with
// the original URL from which the error was returned.
MacaroonPath string `json:",omitempty"`
// CookieNameSuffix holds the desired cookie name suffix to be
// associated with the macaroon. The actual name used will be
// ("macaroon-" + CookieName). Clients may ignore this field -
// older clients will always use ("macaroon-" +
// macaroon.Signature() in hex).
CookieNameSuffix string `json:",omitempty"`
// The following fields are associated with the
// ErrInteractionRequired error code.
// InteractionMethods holds the set of methods that the
// third party supports for completing the discharge.
// See InteractionMethod for a more convenient
// accessor method.
InteractionMethods map[string]*json.RawMessage `json:",omitempty"`
// LegacyVisitURL holds a URL that the client should visit
// in a web browser to authenticate themselves.
// This is deprecated - it is superceded by the InteractionMethods
// field.
LegacyVisitURL string `json:"VisitURL,omitempty"`
// LegacyWaitURL holds a URL that the client should visit
// to acquire the discharge macaroon. A GET on
// this URL will block until the client has authenticated,
// and then it will return the discharge macaroon.
// This is deprecated - it is superceded by the InteractionMethods
// field.
LegacyWaitURL string `json:"WaitURL,omitempty"`
}
// SetInteraction sets the information for a particular
// interaction kind to v. The error should be an interaction-required
// error. This method will panic if v cannot be JSON-marshaled.
// It is expected that interaction implementations will
// implement type-safe wrappers for this method,
// so you should not need to call it directly.
func (e *Error) SetInteraction(kind string, v interface{}) {
if e.Info == nil {
e.Info = new(ErrorInfo)
}
if e.Info.InteractionMethods == nil {
e.Info.InteractionMethods = make(map[string]*json.RawMessage)
}
data, err := json.Marshal(v)
if err != nil {
panic(err)
}
m := json.RawMessage(data)
e.Info.InteractionMethods[kind] = &m
}
// InteractionMethod checks whether the error is an InteractionRequired error
// that implements the method with the given name, and JSON-unmarshals the
// method-specific data into x.
func (e *Error) InteractionMethod(kind string, x interface{}) error {
if e.Info == nil || e.Code != ErrInteractionRequired {
return errgo.Newf("not an interaction-required error (code %v)", e.Code)
}
entry := e.Info.InteractionMethods[kind]
if entry == nil {
return errgo.WithCausef(nil, ErrInteractionMethodNotFound, "interaction method %q not found", kind)
}
if err := json.Unmarshal(*entry, x); err != nil {
return errgo.Notef(err, "cannot unmarshal data for interaction method %q", kind)
}
return nil
}
func (e *Error) Error() string {
return e.Message
}
func (e *Error) ErrorCode() ErrorCode {
return e.Code
}
// ErrorInfo returns additional information
// about the error.
// TODO return interface{} here?
func (e *Error) ErrorInfo() *ErrorInfo {
return e.Info
}
// ErrorToResponse returns the HTTP status and an error body to be
// marshaled as JSON for the given error. This allows a third party
// package to integrate bakery errors into their error responses when
// they encounter an error with a *bakery.Error cause.
func ErrorToResponse(ctx context.Context, err error) (int, interface{}) {
errorBody := errorResponseBody(err)
var body interface{} = errorBody
status := http.StatusInternalServerError
switch errorBody.Code {
case ErrBadRequest:
status = http.StatusBadRequest
case ErrPermissionDenied:
status = http.StatusUnauthorized
case ErrDischargeRequired, ErrInteractionRequired:
switch errorBody.version {
case bakery.Version0:
status = http.StatusProxyAuthRequired
case bakery.Version1, bakery.Version2, bakery.Version3:
status = http.StatusUnauthorized
body = httprequest.CustomHeader{
Body: body,
SetHeaderFunc: setAuthenticateHeader,
}
default:
panic(fmt.Sprintf("out of range version number %v", errorBody.version))
}
}
return status, body
}
func setAuthenticateHeader(h http.Header) {
h.Set("WWW-Authenticate", "Macaroon")
}
type errorInfoer interface {
ErrorInfo() *ErrorInfo
}
type errorCoder interface {
ErrorCode() ErrorCode
}
// errorResponse returns an appropriate error
// response for the provided error.
func errorResponseBody(err error) *Error {
var errResp Error
cause := errgo.Cause(err)
if cause, ok := cause.(*Error); ok {
// It's an Error already. Preserve the wrapped
// error message but copy everything else.
errResp = *cause
errResp.Message = err.Error()
return &errResp
}
// It's not an error. Preserve as much info as
// we can find.
errResp.Message = err.Error()
if coder, ok := cause.(errorCoder); ok {
errResp.Code = coder.ErrorCode()
}
if infoer, ok := cause.(errorInfoer); ok {
errResp.Info = infoer.ErrorInfo()
}
return &errResp
}
// NewInteractionRequiredError returns an error of type *Error
// that requests an interaction from the client in response
// to the given request. The originalErr value describes the original
// error - if it is nil, a default message will be provided.
//
// This function should be used in preference to creating the Error value
// directly, as it sets the bakery protocol version correctly in the error.
//
// The returned error does not support any interaction kinds.
// Use kind-specific SetInteraction methods (for example
// WebBrowserInteractor.SetInteraction) to add supported
// interaction kinds.
//
// Note that WebBrowserInteractor.SetInteraction should always be called
// for legacy clients to maintain backwards compatibility.
func NewInteractionRequiredError(originalErr error, req *http.Request) *Error {
if originalErr == nil {
originalErr = ErrInteractionRequired
}
return &Error{
Message: originalErr.Error(),
version: RequestVersion(req),
Code: ErrInteractionRequired,
}
}
type DischargeRequiredErrorParams struct {
// Macaroon holds the macaroon that needs to be discharged
// by the client.
Macaroon *bakery.Macaroon
// OriginalError holds the reason that the discharge-required
// error was created. If it's nil, ErrDischargeRequired will
// be used.
OriginalError error
// CookiePath holds the path for the client to give the cookie
// holding the discharged macaroon. If it's empty, then a
// relative path from the request URL path to / will be used if
// Request is provided, or "/" otherwise.
CookiePath string
// CookieNameSuffix holds the suffix for the client
// to give the cookie holding the discharged macaroon
// (after the "macaroon-" prefix).
// If it's empty, "auth" will be used.
CookieNameSuffix string
// Request holds the request that the error is in response to.
// It is used to form the cookie path if CookiePath is empty.
Request *http.Request
}
// NewDischargeRequiredErrorWithVersion returns an error of type *Error
// that contains a macaroon to the client and acts as a request that the
// macaroon be discharged to authorize the request.
//
// The client is responsible for discharging the macaroon and
// storing it as a cookie (or including it as a Macaroon header)
// to be used for the subsequent request.
func NewDischargeRequiredError(p DischargeRequiredErrorParams) error {
if p.OriginalError == nil {
p.OriginalError = ErrDischargeRequired
}
if p.CookiePath == "" {
p.CookiePath = "/"
if p.Request != nil {
path, err := httputil.RelativeURLPath(p.Request.URL.Path, "/")
if err == nil {
p.CookiePath = path
}
}
}
if p.CookieNameSuffix == "" {
p.CookieNameSuffix = "auth"
}
return &Error{
version: p.Macaroon.Version(),
Message: p.OriginalError.Error(),
Code: ErrDischargeRequired,
Info: &ErrorInfo{
Macaroon: p.Macaroon,
MacaroonPath: p.CookiePath,
CookieNameSuffix: p.CookieNameSuffix,
},
}
}
// BakeryProtocolHeader is the header that HTTP clients should set
// to determine the bakery protocol version. If it is 0 or missing,
// a discharge-required error response will be returned with HTTP status 407;
// if it is 1, the response will have status 401 with the WWW-Authenticate
// header set to "Macaroon".
const BakeryProtocolHeader = "Bakery-Protocol-Version"
// RequestVersion determines the bakery protocol version from a client
// request. If the protocol cannot be determined, or is invalid, the
// original version of the protocol is used. If a later version is
// found, the latest known version is used, which is OK because versions
// are backwardly compatible.
//
// TODO as there are no known version 0 clients, default to version 1
// instead.
func RequestVersion(req *http.Request) bakery.Version {
vs := req.Header.Get(BakeryProtocolHeader)
if vs == "" {
// No header - use backward compatibility mode.
return bakery.Version0
}
x, err := strconv.Atoi(vs)
if err != nil || x < 0 {
// Badly formed header - use backward compatibility mode.
return bakery.Version0
}
v := bakery.Version(x)
if v > bakery.LatestVersion {
// Later version than we know about - use the
// latest version that we can.
return bakery.LatestVersion
}
return v
}
func isDischargeRequiredError(err error) bool {
respErr, ok := errgo.Cause(err).(*Error)
if !ok {
return false
}
return respErr.Code == ErrDischargeRequired
}

View file

@ -1,113 +0,0 @@
package httpbakery
import (
"context"
"net/http"
"net/url"
"gopkg.in/errgo.v1"
"gopkg.in/httprequest.v1"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery"
)
var _ bakery.ThirdPartyLocator = (*ThirdPartyLocator)(nil)
// NewThirdPartyLocator returns a new third party
// locator that uses the given client to find
// information about third parties and
// uses the given cache as a backing.
//
// If cache is nil, a new cache will be created.
//
// If client is nil, http.DefaultClient will be used.
func NewThirdPartyLocator(client httprequest.Doer, cache *bakery.ThirdPartyStore) *ThirdPartyLocator {
if cache == nil {
cache = bakery.NewThirdPartyStore()
}
if client == nil {
client = http.DefaultClient
}
return &ThirdPartyLocator{
client: client,
cache: cache,
}
}
// AllowInsecureThirdPartyLocator holds whether ThirdPartyLocator allows
// insecure HTTP connections for fetching third party information.
// It is provided for testing purposes and should not be used
// in production code.
var AllowInsecureThirdPartyLocator = false
// ThirdPartyLocator represents locator that can interrogate
// third party discharge services for information. By default it refuses
// to use insecure URLs.
type ThirdPartyLocator struct {
client httprequest.Doer
allowInsecure bool
cache *bakery.ThirdPartyStore
}
// AllowInsecure allows insecure URLs. This can be useful
// for testing purposes. See also AllowInsecureThirdPartyLocator.
func (kr *ThirdPartyLocator) AllowInsecure() {
kr.allowInsecure = true
}
// ThirdPartyLocator implements bakery.ThirdPartyLocator
// by first looking in the backing cache and, if that fails,
// making an HTTP request to find the information associated
// with the given discharge location.
//
// It refuses to fetch information from non-HTTPS URLs.
func (kr *ThirdPartyLocator) ThirdPartyInfo(ctx context.Context, loc string) (bakery.ThirdPartyInfo, error) {
// If the cache has an entry in, we can use it regardless of URL scheme.
// This allows entries for notionally insecure URLs to be added by other means (for
// example via a config file).
info, err := kr.cache.ThirdPartyInfo(ctx, loc)
if err == nil {
return info, nil
}
u, err := url.Parse(loc)
if err != nil {
return bakery.ThirdPartyInfo{}, errgo.Notef(err, "invalid discharge URL %q", loc)
}
if u.Scheme != "https" && !kr.allowInsecure && !AllowInsecureThirdPartyLocator {
return bakery.ThirdPartyInfo{}, errgo.Newf("untrusted discharge URL %q", loc)
}
info, err = ThirdPartyInfoForLocation(ctx, kr.client, loc)
if err != nil {
return bakery.ThirdPartyInfo{}, errgo.Mask(err)
}
kr.cache.AddInfo(loc, info)
return info, nil
}
// ThirdPartyInfoForLocation returns information on the third party
// discharge server running at the given location URL. Note that this is
// insecure if an http: URL scheme is used. If client is nil,
// http.DefaultClient will be used.
func ThirdPartyInfoForLocation(ctx context.Context, client httprequest.Doer, url string) (bakery.ThirdPartyInfo, error) {
dclient := newDischargeClient(url, client)
info, err := dclient.DischargeInfo(ctx, &dischargeInfoRequest{})
if err == nil {
return bakery.ThirdPartyInfo{
PublicKey: *info.PublicKey,
Version: info.Version,
}, nil
}
derr, ok := errgo.Cause(err).(*httprequest.DecodeResponseError)
if !ok || derr.Response.StatusCode != http.StatusNotFound {
return bakery.ThirdPartyInfo{}, errgo.Mask(err)
}
// The new endpoint isn't there, so try the old one.
pkResp, err := dclient.PublicKey(ctx, &publicKeyRequest{})
if err != nil {
return bakery.ThirdPartyInfo{}, errgo.Mask(err)
}
return bakery.ThirdPartyInfo{
PublicKey: *pkResp.PublicKey,
Version: bakery.Version1,
}, nil
}

View file

@ -1,88 +0,0 @@
package httpbakery
import (
"context"
"net/http"
"time"
"gopkg.in/errgo.v1"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
)
// Oven is like bakery.Oven except it provides a method for
// translating errors returned by bakery.AuthChecker into
// errors suitable for passing to WriteError.
type Oven struct {
// Oven holds the bakery Oven used to create
// new macaroons to put in discharge-required errors.
*bakery.Oven
// AuthnExpiry holds the expiry time of macaroons that
// are created for authentication. As these are generally
// applicable to all endpoints in an API, this is usually
// longer than AuthzExpiry. If this is zero, DefaultAuthnExpiry
// will be used.
AuthnExpiry time.Duration
// AuthzExpiry holds the expiry time of macaroons that are
// created for authorization. As these are generally applicable
// to specific operations, they generally don't need
// a long lifespan, so this is usually shorter than AuthnExpiry.
// If this is zero, DefaultAuthzExpiry will be used.
AuthzExpiry time.Duration
}
// Default expiry times for macaroons created by Oven.Error.
const (
DefaultAuthnExpiry = 7 * 24 * time.Hour
DefaultAuthzExpiry = 5 * time.Minute
)
// Error processes an error as returned from bakery.AuthChecker
// into an error suitable for returning as a response to req
// with WriteError.
//
// Specifically, it translates bakery.ErrPermissionDenied into
// ErrPermissionDenied and bakery.DischargeRequiredError
// into an Error with an ErrDischargeRequired code, using
// oven.Oven to mint the macaroon in it.
func (oven *Oven) Error(ctx context.Context, req *http.Request, err error) error {
cause := errgo.Cause(err)
if cause == bakery.ErrPermissionDenied {
return errgo.WithCausef(err, ErrPermissionDenied, "")
}
derr, ok := cause.(*bakery.DischargeRequiredError)
if !ok {
return errgo.Mask(err)
}
// TODO it's possible to have more than two levels here - think
// about some naming scheme for the cookies that allows that.
expiryDuration := oven.AuthzExpiry
if expiryDuration == 0 {
expiryDuration = DefaultAuthzExpiry
}
cookieName := "authz"
if derr.ForAuthentication {
// Authentication macaroons are a bit different, so use
// a different cookie name so both can be presented together.
cookieName = "authn"
expiryDuration = oven.AuthnExpiry
if expiryDuration == 0 {
expiryDuration = DefaultAuthnExpiry
}
}
m, err := oven.Oven.NewMacaroon(ctx, RequestVersion(req), derr.Caveats, derr.Ops...)
if err != nil {
return errgo.Notef(err, "cannot mint new macaroon")
}
if err := m.AddCaveat(ctx, checkers.TimeBeforeCaveat(time.Now().Add(expiryDuration)), nil, nil); err != nil {
return errgo.Notef(err, "cannot add time-before caveat")
}
return NewDischargeRequiredError(DischargeRequiredErrorParams{
Macaroon: m,
CookieNameSuffix: cookieName,
Request: req,
})
}

Some files were not shown because too many files have changed in this diff Show more