Merge pull request #194 from gabriel-samfira/remove-lxd-provider
Remove the LXD internal provider
This commit is contained in:
commit
a13c5db1a7
519 changed files with 95 additions and 95202 deletions
|
|
@ -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/
|
||||
|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
163
config/lxd.go
163
config/lxd.go
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
121
doc/providers.md
121
doc/providers.md
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
20
go.mod
|
|
@ -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
132
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
21
test/integration/config/garm-provider-lxd.toml
Normal file
21
test/integration/config/garm-provider-lxd.toml
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
182
vendor/github.com/cloudbase/garm-provider-common/cloudconfig/cloudconfig.go
generated
vendored
182
vendor/github.com/cloudbase/garm-provider-common/cloudconfig/cloudconfig.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
539
vendor/github.com/cloudbase/garm-provider-common/cloudconfig/templates.go
generated
vendored
539
vendor/github.com/cloudbase/garm-provider-common/cloudconfig/templates.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
207
vendor/github.com/cloudbase/garm-provider-common/cloudconfig/util.go
generated
vendored
207
vendor/github.com/cloudbase/garm-provider-common/cloudconfig/util.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
1
vendor/github.com/flosch/pongo2/.gitattributes
generated
vendored
1
vendor/github.com/flosch/pongo2/.gitattributes
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
* text eol=lf
|
||||
41
vendor/github.com/flosch/pongo2/.gitignore
generated
vendored
41
vendor/github.com/flosch/pongo2/.gitignore
generated
vendored
|
|
@ -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
|
||||
8
vendor/github.com/flosch/pongo2/.travis.yml
generated
vendored
8
vendor/github.com/flosch/pongo2/.travis.yml
generated
vendored
|
|
@ -1,8 +0,0 @@
|
|||
language: go
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
go:
|
||||
- 1.12
|
||||
script:
|
||||
- go test -v
|
||||
11
vendor/github.com/flosch/pongo2/AUTHORS
generated
vendored
11
vendor/github.com/flosch/pongo2/AUTHORS
generated
vendored
|
|
@ -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.
|
||||
20
vendor/github.com/flosch/pongo2/LICENSE
generated
vendored
20
vendor/github.com/flosch/pongo2/LICENSE
generated
vendored
|
|
@ -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.
|
||||
167
vendor/github.com/flosch/pongo2/README.md
generated
vendored
167
vendor/github.com/flosch/pongo2/README.md
generated
vendored
|
|
@ -1,167 +0,0 @@
|
|||
# [pongo](https://en.wikipedia.org/wiki/Pongo_%28genus%29)2
|
||||
|
||||
[](https://pkg.go.dev/flosch/pongo2)
|
||||
[](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)
|
||||
}
|
||||
```
|
||||
137
vendor/github.com/flosch/pongo2/context.go
generated
vendored
137
vendor/github.com/flosch/pongo2/context.go
generated
vendored
|
|
@ -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...)
|
||||
}
|
||||
31
vendor/github.com/flosch/pongo2/doc.go
generated
vendored
31
vendor/github.com/flosch/pongo2/doc.go
generated
vendored
|
|
@ -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
|
||||
91
vendor/github.com/flosch/pongo2/error.go
generated
vendored
91
vendor/github.com/flosch/pongo2/error.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
141
vendor/github.com/flosch/pongo2/filters.go
generated
vendored
141
vendor/github.com/flosch/pongo2/filters.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
927
vendor/github.com/flosch/pongo2/filters_builtin.go
generated
vendored
927
vendor/github.com/flosch/pongo2/filters_builtin.go
generated
vendored
|
|
@ -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(), "&", "&", -1)
|
||||
output = strings.Replace(output, ">", ">", -1)
|
||||
output = strings.Replace(output, "<", "<", -1)
|
||||
output = strings.Replace(output, "\"", """, -1)
|
||||
output = strings.Replace(output, "'", "'", -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
|
||||
}
|
||||
15
vendor/github.com/flosch/pongo2/helpers.go
generated
vendored
15
vendor/github.com/flosch/pongo2/helpers.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
432
vendor/github.com/flosch/pongo2/lexer.go
generated
vendored
432
vendor/github.com/flosch/pongo2/lexer.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
16
vendor/github.com/flosch/pongo2/nodes.go
generated
vendored
16
vendor/github.com/flosch/pongo2/nodes.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
23
vendor/github.com/flosch/pongo2/nodes_html.go
generated
vendored
23
vendor/github.com/flosch/pongo2/nodes_html.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
16
vendor/github.com/flosch/pongo2/nodes_wrapper.go
generated
vendored
16
vendor/github.com/flosch/pongo2/nodes_wrapper.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
26
vendor/github.com/flosch/pongo2/options.go
generated
vendored
26
vendor/github.com/flosch/pongo2/options.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
309
vendor/github.com/flosch/pongo2/parser.go
generated
vendored
309
vendor/github.com/flosch/pongo2/parser.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
59
vendor/github.com/flosch/pongo2/parser_document.go
generated
vendored
59
vendor/github.com/flosch/pongo2/parser_document.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
517
vendor/github.com/flosch/pongo2/parser_expression.go
generated
vendored
517
vendor/github.com/flosch/pongo2/parser_expression.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
14
vendor/github.com/flosch/pongo2/pongo2.go
generated
vendored
14
vendor/github.com/flosch/pongo2/pongo2.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
133
vendor/github.com/flosch/pongo2/tags.go
generated
vendored
133
vendor/github.com/flosch/pongo2/tags.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
52
vendor/github.com/flosch/pongo2/tags_autoescape.go
generated
vendored
52
vendor/github.com/flosch/pongo2/tags_autoescape.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
129
vendor/github.com/flosch/pongo2/tags_block.go
generated
vendored
129
vendor/github.com/flosch/pongo2/tags_block.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
27
vendor/github.com/flosch/pongo2/tags_comment.go
generated
vendored
27
vendor/github.com/flosch/pongo2/tags_comment.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
106
vendor/github.com/flosch/pongo2/tags_cycle.go
generated
vendored
106
vendor/github.com/flosch/pongo2/tags_cycle.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
52
vendor/github.com/flosch/pongo2/tags_extends.go
generated
vendored
52
vendor/github.com/flosch/pongo2/tags_extends.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
95
vendor/github.com/flosch/pongo2/tags_filter.go
generated
vendored
95
vendor/github.com/flosch/pongo2/tags_filter.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
49
vendor/github.com/flosch/pongo2/tags_firstof.go
generated
vendored
49
vendor/github.com/flosch/pongo2/tags_firstof.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
159
vendor/github.com/flosch/pongo2/tags_for.go
generated
vendored
159
vendor/github.com/flosch/pongo2/tags_for.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
76
vendor/github.com/flosch/pongo2/tags_if.go
generated
vendored
76
vendor/github.com/flosch/pongo2/tags_if.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
116
vendor/github.com/flosch/pongo2/tags_ifchanged.go
generated
vendored
116
vendor/github.com/flosch/pongo2/tags_ifchanged.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
78
vendor/github.com/flosch/pongo2/tags_ifequal.go
generated
vendored
78
vendor/github.com/flosch/pongo2/tags_ifequal.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
78
vendor/github.com/flosch/pongo2/tags_ifnotequal.go
generated
vendored
78
vendor/github.com/flosch/pongo2/tags_ifnotequal.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
84
vendor/github.com/flosch/pongo2/tags_import.go
generated
vendored
84
vendor/github.com/flosch/pongo2/tags_import.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
146
vendor/github.com/flosch/pongo2/tags_include.go
generated
vendored
146
vendor/github.com/flosch/pongo2/tags_include.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
132
vendor/github.com/flosch/pongo2/tags_lorem.go
generated
vendored
132
vendor/github.com/flosch/pongo2/tags_lorem.go
generated
vendored
|
|
@ -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.`
|
||||
149
vendor/github.com/flosch/pongo2/tags_macro.go
generated
vendored
149
vendor/github.com/flosch/pongo2/tags_macro.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
50
vendor/github.com/flosch/pongo2/tags_now.go
generated
vendored
50
vendor/github.com/flosch/pongo2/tags_now.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
50
vendor/github.com/flosch/pongo2/tags_set.go
generated
vendored
50
vendor/github.com/flosch/pongo2/tags_set.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
54
vendor/github.com/flosch/pongo2/tags_spaceless.go
generated
vendored
54
vendor/github.com/flosch/pongo2/tags_spaceless.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
68
vendor/github.com/flosch/pongo2/tags_ssi.go
generated
vendored
68
vendor/github.com/flosch/pongo2/tags_ssi.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
45
vendor/github.com/flosch/pongo2/tags_templatetag.go
generated
vendored
45
vendor/github.com/flosch/pongo2/tags_templatetag.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
83
vendor/github.com/flosch/pongo2/tags_widthratio.go
generated
vendored
83
vendor/github.com/flosch/pongo2/tags_widthratio.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
88
vendor/github.com/flosch/pongo2/tags_with.go
generated
vendored
88
vendor/github.com/flosch/pongo2/tags_with.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
276
vendor/github.com/flosch/pongo2/template.go
generated
vendored
276
vendor/github.com/flosch/pongo2/template.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
156
vendor/github.com/flosch/pongo2/template_loader.go
generated
vendored
156
vendor/github.com/flosch/pongo2/template_loader.go
generated
vendored
|
|
@ -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 = ""
|
||||
}()
|
||||
}
|
||||
*/
|
||||
305
vendor/github.com/flosch/pongo2/template_sets.go
generated
vendored
305
vendor/github.com/flosch/pongo2/template_sets.go
generated
vendored
|
|
@ -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
|
||||
)
|
||||
540
vendor/github.com/flosch/pongo2/value.go
generated
vendored
540
vendor/github.com/flosch/pongo2/value.go
generated
vendored
|
|
@ -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]
|
||||
}
|
||||
693
vendor/github.com/flosch/pongo2/variable.go
generated
vendored
693
vendor/github.com/flosch/pongo2/variable.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
187
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/LICENSE
generated
vendored
187
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/LICENSE
generated
vendored
|
|
@ -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.
|
||||
97
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/bakery.go
generated
vendored
97
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/bakery.go
generated
vendored
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
503
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checker.go
generated
vendored
503
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checker.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
246
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers/checkers.go
generated
vendored
246
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers/checkers.go
generated
vendored
|
|
@ -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)
|
||||
}
|
||||
137
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers/declared.go
generated
vendored
137
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers/declared.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
214
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers/namespace.go
generated
vendored
214
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers/namespace.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
97
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers/time.go
generated
vendored
97
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers/time.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
381
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/codec.go
generated
vendored
381
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/codec.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
282
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/discharge.go
generated
vendored
282
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/discharge.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
56
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/dischargeall.go
generated
vendored
56
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/dischargeall.go
generated
vendored
|
|
@ -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
|
||||
})
|
||||
88
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/doc.go
generated
vendored
88
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/doc.go
generated
vendored
|
|
@ -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
|
||||
77
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/error.go
generated
vendored
77
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/error.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
219
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/keys.go
generated
vendored
219
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/keys.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
28
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/logger.go
generated
vendored
28
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/logger.go
generated
vendored
|
|
@ -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{}) {}
|
||||
356
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/macaroon.go
generated
vendored
356
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/macaroon.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
}
|
||||
359
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/oven.go
generated
vendored
359
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/oven.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
134
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/slice.go
generated
vendored
134
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/slice.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
63
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/store.go
generated
vendored
63
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/store.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
30
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/version.go
generated
vendored
30
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/version.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
}
|
||||
200
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/browser.go
generated
vendored
200
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/browser.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
157
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/checkers.go
generated
vendored
157
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/checkers.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
727
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/client.go
generated
vendored
727
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/client.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
12
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/context_go17.go
generated
vendored
12
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/context_go17.go
generated
vendored
|
|
@ -1,12 +0,0 @@
|
|||
// +build go1.7
|
||||
|
||||
package httpbakery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func contextFromRequest(req *http.Request) context.Context {
|
||||
return req.Context()
|
||||
}
|
||||
12
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/context_prego17.go
generated
vendored
12
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/context_prego17.go
generated
vendored
|
|
@ -1,12 +0,0 @@
|
|||
// +build !go1.7
|
||||
|
||||
package httpbakery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func contextFromRequest(req *http.Request) context.Context {
|
||||
return context.Background()
|
||||
}
|
||||
367
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/discharge.go
generated
vendored
367
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/discharge.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
359
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/error.go
generated
vendored
359
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/error.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
113
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/keyring.go
generated
vendored
113
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/keyring.go
generated
vendored
|
|
@ -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
|
||||
}
|
||||
88
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/oven.go
generated
vendored
88
vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/oven.go
generated
vendored
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue