1. Project Overview & Quickstart (emissary-ingress/emissary)
File: README.md
Emissary-ingress
Emissary-Ingress is an open-source Kubernetes-native API Gateway +
Layer 7 load balancer + Kubernetes Ingress built on Envoy Proxy.
Emissary-ingress is a CNCF incubation project (and was formerly known as Ambassador API Gateway).
Emissary-ingress enables its users to:
- Manage ingress traffic with load balancing, support for multiple protocols (gRPC and HTTP/2, TCP, and web sockets), and Kubernetes integration
- Manage changes to routing with an easy to use declarative policy engine and self-service configuration, via Kubernetes CRDs or annotations
- Secure microservices with authentication, rate limiting, and TLS
- Ensure high availability with sticky sessions, rate limiting, and circuit breaking
- Leverage observability with integrations with Grafana, Prometheus, and Datadog, and comprehensive metrics support
- Enable progressive delivery with canary releases
- Connect service meshes including Consul, Linkerd, and Istio
- Knative serverless integration
See the full list of features here.
Branches
(If you are looking at this list on a branch other than main, it
is likely to be wrong.)
main: main Emissary-ingress development work (:heavy_check_mark: active development)master: frozen branch for Emissary-ingress 3.10 (:heavy_check_mark: frozen)release/v2.5: frozen branch for Emissary-ingress 2.5 (:heavy_check_mark: frozen)
Building Emissary 4
gmake images should make all the images you need using
goreleaser. Note that .goreleaser.yaml is a
generated file; do not edit it directly. It's this way so that we
can use the open-source version of goreleaser.
If you want to update the Envoy version, edit the ENVOY_IMAGE value in
the Makefile.
Architecture
Emissary is configured via Kubernetes CRDs, or via annotations on
Kubernetes Services. Internally, it uses the Envoy Proxy to actually
handle routing data; externally, it relies on Kubernetes for scaling and
resiliency.
Getting Started
The fastest way to get started with Emissary is to follow the
Quickstart. For other common questions, check out the Emissary FAQ or
the full Emissary documentation.
Community
Emissary-ingress is a CNCF Incubating project and welcomes any and all
contributors.
At this point, Emissary's original parent company has stepped back from
direct involvement in the project, so the community is now more
important than ever. As we've said at the past few KubeCons, Emissary
needs you.
The best way to join the community is to join the CNCF Slack and hop
into the #emissary-ingress channel. Checking out [open issues] or
[contributing documentation] are both great ways to get started
contributing.
You can also check out the project's governance documentation:
CODE_OF_CONDUCT.md: the community
code of conductGOVERNANCE.md: the governance structureMAINTAINERS.md: the list of maintainersMEETING_SCHEDULE.md: the schedule
of community meetings
File: build-aux/docs/conventions.md
Conventions for use in build-aux
Each .mk snippet starts with a reference header-comment of the
format:
```make
# Copyright statement.
#
# A sentence or two introducing what this file does.
#
## Section heading ##
# - type: item
# - type: item
## Section heading ##
# - item
# - item
#
# High-level prose documentation.
```Always include (at a minimum) these 4 sections, even if their content
is (none):
## Eager inputs ##(mostly variables) Eager-evaluated inputs that
need to be defined before loading the snippet## Lazy inputs ##(mostly variables) Lazy-evaluated inputs can be
defined before or after loading the snippet## Outputs ##(targets, variables, et c.)## common.mk targets ##
If there are which targets from snippets other than common.mk that
it hooks in to, a section for each of those snippets should exist too.
The most common reason for an input to be eager is if it is listed as
a dependency of a target defined in the .mk file.
Bullet points under "Eager inputs", "Lazy inputs", or "Outputs" should
be of the format "Type: thing [extra info]", optionally with the ":"
aligned between rows. Types currently used are:
Target(s)(only for use as an output): Indicates that the snippet
defines rules to create the specified file(s). Prefer to list each
target separately; only use the pluralTargetswhen listing an
expression that govers a large range of actual files..PHONY Target(s)(only for use as an output): LikeTarget, but
is declared as.PHONY; a file with that name is never actually
created.File(only for use as an input): Indicates that this file is
parsed by the Makefile snippet. Note that it should not be a file
described by a Target, since it needs to be there at
Makefile-parse-time.Variable: The meaning is obvious.- For inputs: If there is a default value, the listing should
include?= valuefor a possibly pseudo-code or commentvalue
documenting what the default value is. - For outputs: The listing should include
= valueor?= value
for a possibly pseudo-code or commentvaluedocumenting what
the variable is set to contain. The=should be?=as
appropriate, to document whether it can be overridden by the
caller/user. - If a
?=variable is listed in "Outputs", consider also listing
it in "inputs"; if an input gets a default value set, consider
also listing it in "Outputs".NAMEingo-mod.mk. It isn't listed as an an input, because
it isn't one "in spirit".- "Executables" (below) are set with
?=, but don't list them as
"inputs".
- For inputs: If there is a default value, the listing should
Function: (only for use as an output) This is a special case of a
Variable that defines a function to be called with$(call funcname,args). Should not be listed as an input; if you depend
on a function, just go ahead and include the snippet that defines
it.Executable(only for use as an output): An "Executable" output is
a special-case of both a "Target" output and a "Variable"
input/output. An "Executable" output has the following attributes:- It is exposed as a variable that stores an absolute path to an
executable file (this makes it safe to list as a dependency). - That variable can be overridden using an environment variable
(that is: it is set with?=). - It is NOT guaranteed to already exist (you may need to depend on
it), but a rule to create it is guaranteed be there it it doesn't
already exist. - Overriding it using an environment variable will not affect the
rule to create the standard version, or cleanup rules to remove
it; those targets do not obey the environment variable.
- It is exposed as a variable that stores an absolute path to an
File: build-aux/docs/intro.md
Datawire build-aux Introduction
build-aux is a collection of Makefile .mk snippets (and associated
utilities). Each of the .mk snippets is written to be as
independent from the others as possible, but integrate well by
following common conventions. You may pick-and-choose which to
include simply by writing include build-aux/NAME.mk in yourMakefile.
Each .mk snippet starts with a reference header-comment
identifying:
- any eager-evaluated inputs (mostly variables)
- any lazy-evaluated inputs (mostly variables)
- any outputs (targets, variables)
- which targets from other snippets it hooks in to (mostly hooking
in tocommon.mktargets)
Eager inputs need to be defined before loading the snippet; lazy
inputs can be defined later. See [conventions.mk][./conventions.md]
for more information on the reference header-comment.
For the most part, you don't need to worry about dependencies between.mk files; each file will automatically include the others it
depends on. However, if you would like to use an output from one
snippet as an eager input to another, then you do need to worry about
include order; if you would like to use kubernaut-ui.mk to setKUBECONFIG for teleproxy.mk, then you will need to make sure you
include kubernaut-ui.mk before you include teleproxy.mk. You
don't need to worry about including a file twice; this is safe, as
they all have C-header-style include guards.
`common.mk`
Most (but not all) of the snippets include common.mk, which
Configures Make to use sane settings (since the default settings
are set for historical compatibility, not the "right thing").Defines several
$(call โฆ)-able helper functions, likejoinlist,path.trimprefix, orquote.shell. Seecommon.mkitself for the full list, and
documentation on how to use them.Declares several utility variables:
GOOS,GOARCH: Operating system name, and CPU architecture.GOis included in the variable names to indicate that they use
the same names asgo env(as opposed to usinguname -s/uname -mnames, or something else).NL: a single newline character, since that's hard to type in MakeSPACE: a single space character, since that's hard to type in
Make
Declares several high-level common targets, like
make build,make clean, ormake check. Seecommon.mk
itself for the full list. By defining a list of common
user-facing targets, the rest of the snippets can hook in to that
and provide a uniform interface. With the exception ofmake check(which is special, see below), yourMakefilecan extend
any of these by adding dependencies to the target name. For
example, to havemake buildbuild thefoofile, you could
write:textbuild: fooor to have
make lintrun theflake8Python linter, you could
writetextlint: flake8 flake8: flake8 mypackage/ .PHONY: flake8or
textlint: flake8 mypackage/The former has the advantage that it allows you to add multiple
hooks on tolint(so it's the only method that build-aux
snippets use), and also allows flake8 to run in parallel with
other linters set up by build-aux snippets.make checkis special in how it works. Instead of writingtextcheck: my-test # WRONGyou write
texttest-suite.tap: my-test.tap # CORRECTSee
testing.mdfor information about writing the
recipe formy-test.tap.Because of
make check's use oftest-suite.tap,common.mk
also goes ahead and hasmake cleanrunrm -f test-suite.tap.With the exceptions of
make checkandmake clean,common.mk
only provides empty definitions; it is up to yourMakefile, or
other.mksnippets to make these rules actually do something.
`help.mk`
The snippet help.mk adds a make help target, that display
information about your project (customizable by setting thehelp.body variable from your Makefile), and a table of all of themake FOO targets that Make knows about. Any targets you write in
your Makefile that you think should be visible can be added to this
listing by writing a magic comment:
frob: ## Frobnicate the splorksSee help.mk itself for full documentation.
File: build-aux/docs/testing.md
Datawire build-aux Test Harness
common.mk includes a built-in test harness that allows you to plug
in your own tests, and have them aggregated and summarized, like:
$ make check
...
PASS: go-test 5 - github.com/datawire/apro/cmd/amb-sidecar.TestAppNoToken
PASS: go-test 6 - github.com/datawire/apro/cmd/amb-sidecar.TestAppBadToken
PASS: go-test 7 - github.com/datawire/apro/cmd/amb-sidecar.TestAppBadCookie
PASS: go-test 8 - github.com/datawire/apro/cmd/amb-sidecar.TestAppCallback
PASS: go-test 9 - github.com/datawire/apro/cmd/amb-sidecar.TestAppCallbackNoCode
PASS: tests/local/apictl.tap.gen 1 - check_version
============================================================================
test-suite summary
============================================================================
# TOTAL: 10
# SKIP: 0
# PASS: 10
# XFAIL: 0
# FAIL: 0
# XPASS: 0
# ERROR: 0
============================================================================
make[1]: Leaving directory '/home/lukeshu/src/apro'(If you were viewing that in a terminal, it would also be pretty and
colorized.)
Each test-case can have one of 6 results:
pass,fail,skip: You can guess what these mean.xfail("expected fail"): The test failed, but it was expected to
fail. Perhaps you wrote a test for a feature for before
implementing the feature. Perhaps you wrote a test that captures a
bug report, but haven't written the fix yet. Makes
Test-Driven-Development possible.xpass("unexpected pass"): The test passed, but it was expected
to xfail. This either means you did a poor job implementing the
test, and it doesn't really check what you think it checks, or it
means you've implemented the fix, but forgot to replacexfail
withfailin the test.error: It isn't that the test decided that there's bug in the
code-under-test, it's that the test itself encountered an error.
You can use any test framework or language you like, as long as it can
emit TAP, the Test Anything Protocol (or something that can be
translated to TAP). Both TAP version 12 and version 13 are supported.not ok # TODO is "xfail", while , ok # TODO is "xpass"
Side-Note: pytest-tap emits
ok # TODOfor both xfail and xpass,
which is wrong, and I consider to be a bug in pytest-tap.
Built-in test runners
By default, common.mk knows how to run test cases of 2 types (but
you can easily add more):
*.testGNU Automake-compatible standalone test cases. One file
is one test case.FOO.testmust be an executable file. It is
run with no arguments, stdout and stderr are ignored (but logged toFOO.log); it is the exit code that determines the test result:- 0 => pass
- 77 => skip
- anything else => fail
It is not possible to xfail or xpass with this type of test.
*.tap.genTAP-emitting test cases. You may have multiple test
cases per file.FOO.tap.genmust be an executable file. It is
run with no arguments; stdout and stderr are merged, and are taken
to be a TAP stream (both v12 and v13 are supported). The exit code
is ignored.
common.mk does not scan your source directory for tests (butgo-*.mk will scan for go test tests). In your Makefile You must
explicitly tell it about any .test or .tap.gen files that you
would like it to include in make check. For example, if you would
like it to include any .test or .tap.gen files in the ./tests/
directory, you could write:
test-suite.tap: $(patsubst %.test,%.tap,$(wildcard tests/*.test))
test-suite.tap: $(patsubst %.tap.gen,%.tap,$(wildcard tests/*.tap.gen))Adding your own test runners
To add a new test runner, you just need a command that emits TAP:tee it to a .tap file, and pipe that to $(tools/tap-driver) stream -n TEST_GROUP_NAME to pretty-print the results as they happen:
# Tell Make how to run the test command, and stream the results to
# `$(tools/tap-driver) stream` to pretty-print the results as they happen.
my-test.tap: my-test.input $(tools/tap-driver) FORCE
@SOME_COMMAND_THAT_EMITS_TAP 2>&1 | tee $@ | $(tools/tap-driver) stream -n my-test
# Tell Make to include 'my-test' in `make check`
test-suite.tap: my-test.tapFor example, to use BATS (Bash Automated Testing System), you
would write:
%.tap: %.bats $(tools/tap-driver) FORCE
@bats --tap $< | tee $@ | $(tools/tap-driver) stream -n $<
# Automatically include `./tests/*.bats`
test-suite.tap: $(patsubst %.bats,%.tap,$(wildcard tests/*.bats))If your test framework of choice doesn't support TAP output, you can
pipe it to a helper program that can translate it. For example, go test doesn't support TAP output, but go test -json output is
parsable, so we pipe that to gotest2tap, which
translates it to TAP.
If you set SHELL = sh -o pipefail in your Makefile (the pros and
cons of which I won't comment on here), you should be sure that if
your test-runner indicates success or failure with an exit code, that
you ignore that exit code:
%.tap: %.bats $(tools/tap-driver) FORCE
@{ bats --tap $< || true; } | tee $@ | $(tools/tap-driver) stream -n $<Adding dependencies of tests
It is assumed that all tests depend on make build. To add a
dependency shared by all tests, to declare a dependency that all tests
should depend on, declare it as a dependency of check itself. For
example, common.mk says:
check: lint buildAs another example, the Makefile for Ambassador Pro says:
check: $(if $(HAVE_DOCKER),deploy proxy)To declare a dependency for an individual test is a little trickier,
because you must keep in mind what type of test it is. For .tap.gen
tests, you must declare it both on the .tap file and (if the
dependency is not a .tap) on check itself:
test-suite.tap: tests/cluster/oauth-e2e.tap
check tests/cluster/oauth-e2e.tap: tests/cluster/oauth-e2e/node_modulesIf that were a .test test, you would need to declare it on the.log file instead of .tap:
test-suite.tap: tests/cluster/oauth-e2e.tap
check tests/cluster/oauth-e2e.log: tests/cluster/oauth-e2e/node_modulesIf you need one test to depend on another test, write the dependency
using the .tap suffix (not .log). You do not need to write the
depenency for check, since it will already depend on the .tap
through test-suite.tap:
test-suite.tap: foo.tap bar.tap
foo.log: bar.tapFile: build-aux/README.md
Datawire build-aux
This is a collection of Makefile snippets (and associated utilities)
for use in Datawire projects.
It has the following downsides:
- It does not support out-of-tree builds.
- It has no notion of nesting. You cannot
cdto a sub-directory,
runmake, and have it just build the stuff in that directory. - It has no notion of nesting. If you want per-directory build
descriptions, you'll have to build that functionality yourself, or
offload it to a separate build-system, likegoorsetup.py. - Most of the
.mksnippets have a hard dependency on thego
program being inPATH, even if none of the sources are Go.
If any of those are a bummer, but you still want the "Make snippets in
a ./build-aux/ folder" concept, consider
Autothing (which has the
downsides that it requires GNU Make 3.82 or above, and is slow).
At Datawire, those are good trade-offs, since:
- We're mostly a Python and Go shop (we don't care about #2 or #3,
and only care about #4 for Python-only projects). - I've never heard anyone here even mention out-of-tree builds (#1).
- We need to support macOS (which still ships GNU Make 3.81, as it's
the last GPLv2 version) (so Autothing is ruled out).
How to use
Add build-aux.git as build-aux/ in the git repository of the
project that you want to use this from. I recommend that you do this
using git subtree, but git submodule is fine too.
Then, in your Makefile, write include build-aux/FOO.mk for each
common bit of functionality that you want to make use of.
Using `git-subtree` to manage `./build-aux/`
Start using build-aux:
text$ git subtree add --squash --prefix=build-aux [email protected]:datawire/build-aux.git masterUpdate to latest build-aux:
text$ ./build-aux/build-aux-pullPush "vendored" changes upstream to build-aux.git:
text$ ./build-aux/build-aux-push
Documentation
- Each
.mksnippet contains a reference-quality header comment
identifying- any "eager" inputs (mostly variables); these must be defined
before including the.mkfile. - any "lazy" inputs (mostly variables); these may be defined
before or after including the.mkfile. - any outputs (targets, variables)
- which targets from other snippets it hooks in to (mostly hooking
in tocommon.mktargets)
- any "eager" inputs (mostly variables); these must be defined
docs/intro.mdis an introduction to
big-picture ideas in*.mksnippets.docs/golang.mddiscusses support for building
software written in Go.docs/testing.mddiscusses adding tests tomake check.HACKING.mdhas guidelines for contributing tobuild-aux.git.
File: cmd/kubestatus/README.md
The kubestatus program provides a command line interface for updating
the status of a kubernetes resource.
The kubestatus program operates on a set of resources. This set is
defined by the combination of the
and the --field-selector and --label-selector arguments, e.g.:
kubestatus service -f metadata.name=fooNote that by default kubestatus will just list the statuses you have
selected. If you want to update the status, you need to supply it with
a new status via the -u or --update flag:
kubestatus service -f metadata.name=foo -u updated-status.jsonThe status must be supplied in a valid json format in the referenced
file. This status also needs to conform to whatever the kubernetes
schema is for the given resource. You can use kubectl explain to
figure out what that is, e.g.:
kubectl explain service.status
...
kubectl explain service.status.loadBalancer
...File: pkg/ambex/README.md
Ambex: Ambassador Experimental^H^H^H^H^H^H^H^H^H^H^H^H ADS service
Ambassador prior to v0.50.0 worked by writing out Envoy v1 JSON
configuration files, then triggering a hot restart of Envoy. This
works, but it has some unpleasant limitations:
- Restarts take awhile, and as a result you can't change the
configuration very quickly. - Restarts can drop connections (cf Envoy #2776; more on this later).
- Envoy itself is deprecating the v1 configuration, and will only
support v2 in a bit.
To get around these limitations, and generally go for a better
experience all 'round, we want to switch to the so-called xDS model,
in which Envoy's configuration is supplied by its various "Discovery
Services": e.g. the CDS is the Cluster Discovery Service; theEDS is the Endpoint Discovery Service. For Ambassador, the
Aggregated Discovery Service or ADS is the one we want to use --
basically, it brings the other services together under one aegis and
lets you just tell Envoy "get everything dynamically."
However, the whole ADS thing is a bit of a pain:
- Envoy makes a bidirectional gRPC stream to the
ADSserver. - The
ADSthen makes gRPC calls to the Envoy to feed the Envoy
configuration elements, but: - The
ADShas to carefully order things such that the configuration
elements match what Envoy expects for consistency.
Rather than do all that logic by hand, we'll use the Envoygo-control-plane[^1] for the heavy lifting. This is also something of a
pain, given that it's not well documented, but here's the deal:
The root of the world is a
SnapshotCache:import github.com/datawire/ambassador/pkg/envoy-control-plane/cache,
then refer tocache.SnapshotCache.- A collection of internally consistent configuration objects is a
Snapshot(cache.Snapshot). Snapshots are collected in theSnapshotCache.- A given
SnapshotCachecan hold configurations for multiple
Envoys, identified by the EnvoynodeID, which must be configured
for the Envoy.
The
SnapshotCachecan only holdgo-control-planeconfiguration
objects, so you have to build these up to hand to theSnapshotCache.The gRPC stuff is handled by a
Server:import github.com/datawire/ambassador/pkg/envoy-control-plane/server,
then refer toserver.Server.- Our
runManagementServerfunction (largely ripped off from thego-control-planetests) gets this running. It takes aSnapshotCache(cleverly calledconfigfor no reason I (Flynn)
understand) and a standard GogRPCServeras arguments. - ALL the gRPC madness is handled by the
Server, with the
assistance of the methods in itscallbackobject.
Once the
Serveris running, Envoy can open a gRPC stream to it.- On connection, Envoy will get handed the most recent
Snapshot
that theServer'sSnapshotCacheknows about. - Whenever a newer
Snapshotis added to theSnapshotCache, thatSnapshotwill get sent to the Envoy.
- On connection, Envoy will get handed the most recent
We manage the
SnapshotCacheby loading Envoy configuration files
on disk:- it ignores files that start with a
.(hidden files) - it interprets
*.jsonfiles as JSON-encoded protobuf - it interprets
*.pbfiles as text-encoded protobuf - all other files are ignored
As for when it loads those files: - By default when we get a SIGHUP we reload the configuration.
- When passed the
--watchargument we reload whenever any file in
the directory changes. Be careful about updating files
atomically if you use this!
- it ignores files that start with a
[^1]: The Envoy go-control-plane usually refers to
github.com/envoyproxy/go-control-plane, but we've "forked" it
as github.com/datawire/ambassador/pkg/envoy-control-plane in
order to build it against the protobufs for our patched Envoy.
Running Ambex
You'll need the Go toolchain, and will want to have a functioning envoy.
Then, you can run the ambex CLI using busyambassador:
go run github.com/datawire/ambassador/cmd/busyambassador ambex ARGs...If you're on a platform other than GNU/Linux, in order to have a
functioning envoy, you may want to run all of this in the builder
shell: make shell.
Try it out
You'll want to run both ambex and an instance of envoy with a
boostrap config pointing at that `ambex.
First, start the
ambex:shellgo run github.com/datawire/ambassador/cmd/busyambassador ambex ./example/ambex/or
shellgo run github.com/datawire/ambassador/cmd/busyambassador ambex --watch ./example/ambex/Second, in another shell, start the
envoy:shellenvoy -l debug -c ./example/envoy/bootstrap-ads.yaml
You should now be able to run some curls in (yet) another shell:
$ curl localhost:8080/hello
Hello ADS!!!
$ curl localhost:8080/get
{
"args": {},
"headers": {
"Accept": "*/*",
"Connection": "close",
"Host": "httpbin.org",
"User-Agent": "curl/7.54.0",
"X-Envoy-Expected-Rq-Timeout-Ms": "15000"
},
"origin": "72.74.69.156",
"url": "http://httpbin.org/get"
}Edit and/or add more files to the ./example/ambex/ directory in
order to play with more configurations and see them reload
instantaneously (if you used the --watch flag), or when-triggered
(if you didn't use the --watch flag; trigger a relead by signaling
the process with killall -HUP ambex).
Clean up
Kill Ambex and Envoy with Ctrl-C.
File: python/kat/README.md
kat
Kubernetes Acceptance Testing tool
This is a testing tool that was originally developed for testing of
ambassador, but could potentially be useful for any sort of
application built on top of kubernetes.
This tool streamlines the following process:
- apply a bunch of yaml
- wait for services to spin up
- run a bunch of queries against the spun up APIs
- perform a bunch of assertions on those query results
We originally built these sort of tests in a unit-testing style
framework where each test would proceed through this sequence on its
own. This turns out to be very slow since steps two and three are
highly synchronous/chatty.
This test framework runs these sorts of tests several orders of
magnitude faster by a) applying the yaml for all tests in a single big
batch up front, and b) performing all the queries in step 3
asynchronously using a high performance client.
Please see the tutorial to get started.
File: python/README.md
Ambassador Developer's Guide
Concepts
Ambassador sits between users and an Envoy. The primary job that an Ambassador does is to take an Ambassador configuration and, from that, generate an Envoy configuration. This generation happens using an intermediate representation (IR) to manage all the internal logic that Ambassador needs:
Ambassador config => IR => Envoy configAmbassador Components and Ports
Ambassador comprises several different components:
| Component | Type | Function. |
|---|---|---|
diagd |
Python | Increasingly-misnamed core of the system; manages changing configurations and provides the diagnostics UI |
entrypoint/ambex |
Go | Envoy go-control-plane implementation; supplies Envoy with current configuration |
entrypoint/watcher |
Go | Service/secret discovery; interface to Kubernetes and Consul |
envoy |
C++ | The actual proxy process |
entrypoint, diagd, and envoy are all long-running daemons. If any of them exit, the pod as a whole will exit.
Ambassador uses several TCP ports while running. All but one of them are in the range 8000-8499, and any future assignments for Ambassador ports should come from this range.
| Port | Process | Function |
|---|---|---|
| 8001 | envoy |
Internal stats, logging, etc.; not exposed outside pod |
| 8002 | entrypoint/watcher |
Internal watt snapshot access; not exposed outside pod |
| 8003 | entrypoint/ambex |
Internal ambex snapshot access; not exposed outside pod |
| 8004 | diagd |
Internal diagd access when AMBASSADOR_FAST_RECONFIGURE is set; not exposed outside pod |
| 8005 | entrypoint/external_snapshot_server |
Exposes configuration snapshots for integration with other tools |
| 8006 | envoy |
Default ready listener port |
| 8080 | envoy |
Default HTTP service port |
| 8443 | envoy |
Default HTTPS service port |
| 8877 | diagd |
Direct access to diagnostics UI; provided by busyambassador entrypoint when AMBASSADOR_FAST_RECONFIGURE is set |
The Ambassador Configuration
An Ambassador configuration is a collection of Ambassador configuration resources, which are represented by subclasses of ambassador.config.ACResource. The configuration as a whole is represented by an ambassador.Config object.
ambassador.Config does not know how to parse YAML, interact with Kubernetes, or look at the filesystem. Instead, its consumer must construct a consistent list of fully-instantiated ACResource objects and tell ambassador.Config to load these resources:
aconf: Config = Config()
aconf.load_all(resources: List[ACResource])load_all is only meant to be called once, with a complete set of all the resources comprising the Ambassador configuration. To change the configuration, instantiate a new ambassador.Config.
The ambassador.Config class is relatively unsophisticated: for the most part, all it does is to save the resources handed to it in a way that preserves the type of the resources, and permits consumers to query the Config for various kinds of resources:
get_config(self, key: str) -> AnyFetches all the configuration information the
Confighas for the type tagged askey, e.g.aconf.get_config("mappings")will fetch all theMappings that theConfighas stored.Current keys include:
auth_configs: information fromAuthServicedefinitionsmappings: information fromMappingsmodules: information fromModules, including theAmbassadorandTLSModulesratelimit_configs: information aboutRateLimitServicestracing_configs: information aboutTracingServices
get_module(self, module_name: str) -> Optional[ACResource]Fetches the
Modulewith a given name (e.g.aconf.get_module("ambassador")will fetch information about theAmbassadorModule). ReturnsNoneif noModulewith the given name is found.module_lookup(self, module_name: str, key: str, default: Any=None) -> AnyLooks up a specific
keyin a specificModule. e.g.aconf.module_lookup('ambassador', 'service_port', 8080)will look up the
service_portfrom theAmbassadorModule; if noservice_portis defined, the default value will be 8080.dump(self, output=sys.stdout) -> NoneDumps the entire
Configobject to the givenoutput, for debugging.
Once a Config has loaded resources, it can be used to create an IR.
The IR
The Intermediate Representation is where most of the logic of Ambassador lives. The IR as a whole is represented by an ambassador.IR object, which contains many objects descended from ambassador.ir.IRResource. The logic needed to synthesize the IR from an ambassador.Config is mostly contained in these IRResource subclasses; relatively little is in the ambassador.IR class itself.
An ambassador.IR can only be instantiated from an ambassador.Config:
ir = IR(aconf: Config)The general rule in the IR is that everything interesting is stored in objects descended from IRResource. There are two cases:
Only One `IRResource`
Only one of some IRResource objects can exist within a given IR, no matter how many distinct ACResources they collect information from. A good example here is the IRAmbassador object, which contains global configuration information pooled from the Ambassador Module as well any TLS Module: only one of these will ever exist.
For these single-instance objects:
First the resource is instantiated with the
ambassador.IRandambassador.Configobjects as parameters. Whatever information is needed to initialize theIRResource, its__init__method should pull directly from theConfigand/orIRobjects.Next, the resource's
setupmethod is called, again with theIRandConfigas parameters.setupcan perform further initialization, consistency checks, etc., and must return a boolean:Truewill allow the resource to become active,Falsewill mean the resource isn't needed after all. (For example, theIRAuthobject will returnFalseif it doesn't find any configured authentication services.)The default
setupmethod simply returnsTrue, so anyIRResourcesubclass that doesn't override it will always be active.If the resource's
setupreturnsTrue, theIRwill save it as an active resource.After all resources have been probed, the
IRwill walk the list of active resources and call theadd_mappingsmethod for each active resource.add_mappingsshould add any mappings or clusters needed by the resource. (For example, theIRAmbassadorobject adds mappings (and thus clusters) to handle probes and diagnostics.)The default
add_mappingsmethod does nothing, so no mappings or clusters will be added forIRResourcesubclasses that don't overrideadd_mappings.
For single-instance objects, the distinction between __init__ and setup is rarely relevant. It's OK to do everything in __init__ and allow the default setup to always return True; likewise it's OK to just save incoming data in __init__ and have all logic in setup.
Multiple `IRResource`s
There can be multiple instances of some IRResource objects: for example, a single Config can contain many Mappings and multiple Listeners. For these resources, we use a Factory class, which must implement the load_all classmethod and may implement the finalize classmethod, both of which receive the IR and Config as parameters.
load_allis called early in the instantiation process, and is responsible for creating as many individual resources as needed and saving them (usually in theIRitself).finalizeis called only after all the factories have hadload_allcalled and all other single-instance resources have hadadd_mappingscalled, and is responsible for any normalization or other initialization that depends on global knowledge (for example,MappingFactory.finalizedoes the normalization of weights acrossMappinggroups).
The Full Sequence
The full IR instantiation sequence can be found in ambassador.ir.IR.__init__:
- TLS defaults
IRAmbassadorTLS(TLS contexts, etc.)IRAmbassador(global Ambassador config)IRAuth(extauth services)IRRateLimit(rate limiting)ListenerFactory(listeners -- createsIRListenerobjects)MappingFactory(mappings -- createsIRMappingGroup,IRMapping, andIRClusterobjects)- Cluster naming normalization
Helpers
IR.add_mappingadds or looks up anIRMappingGroupwith associatedIRMappings andIRClusters.IR.add_clusteradds only anIRCluster.IR.has_clusterandIR.get_clusterdoIRClusterlookups.IR.dumpdumps the entire IR for debugging.
The Envoy V1 Configuration
Finally, an Envoy V1 configuration is represented by ambassador.envoy.V1Config. A V1 config is built from an IR, again with most of the logic to do so contained in classes that mirror the structure of the V1 config. As we support later Envoy configuration versions, they will have their own classes.
The root of the Envoy V1 configuration is ambassador.envoy.v1.V1Config.
Overall Life Cycle
Construct a collection of
ACResourceobjects (from disk, from K8s, whatever).This will mostly involve
ACResource.from_dictorACResource.from_yaml.Instantiate an
ambassador.Config. Use itsload_all()method to load up the collection ofACResourceobjects.Instantiate an
ambassador.IRfrom theambassador.Config.Instantiate an
ambassador.envoy.V1Configfrom theambassador.IR.
Developing in Ambassador
In all cases, understanding the class hierarchy and the lifecycle around the IR will be important. Both of these are discussed below.
Adding Features
Adding a feature will start with the Ambassador configuration resources:
- The simple case will involve modifying a schema file and possibly modifying an
ACResourceclass. - The less simple case will involve adding a new schema and a new
ACResourcesubclass.- Unless the new class needs complex logic (it shouldn't), you can just let the existing
Configcode save your new resource. - If it does need complex logic, you'll need to add a handler method to
Config.
- Unless the new class needs complex logic (it shouldn't), you can just let the existing
Once the Ambassador config is dealt with, you'll add or modify the IR to cope. Most of what you do here should involve the IRResource subclasses, not the IR class itself (although if you're adding something completely new, you'll need to add code to IR.__init__ to call your new class).
Once the IR is dealt with, you'll need to add or modify the V1Config to cope with the IR changes.
Handling Bugs
The trick with bugs will be figuring out what you need to change. In general, work from V1Config to IR to Ambassador config -- the closer to Envoy that the fix can go, the simpler it will probably be.
ambassador.Configandambassador.IRboth havedumpmethods that are invaluable for studying their contents.- You can also attach debuggers and look at objects. Most of the things you're working with are subclasses of
dict, so many introspection tools are simple.
Class Hierarchy
IRIRResourceIRAdminIRAmbassadorIRClusterIRFilterIRAuth
IRListenerIRMappingIRMappingGroupIRRateLimitIRAmbassadorTLSIREnvoyTLS
envoyV1ConfigV1AdminV1ClusterV1ClusterManagerV1Listener
(This diagram is mostly about the way the classes are used, rather than strictly reflecting implementation. For example, the Config class is actually ambassador.config.config.Config but is imported into ambassador to make usage easier.)
The `Resource` Class
IRResource and ACResource are subclasses of ambassador.Resource, although they are shown in the packages where they are logically used. A Resource is a kind of dict that can keep track of where it came from, what makes use of it, and any errors associated with it.
To initialize a Resource requires:
rkey: a short identifier that is used as the primary key for all the Ambassador classes to identify this single specific resource. It should be something like "ambassador-default.1" or the like that is very specific, though it doesn't have to be fun for humans.location: a more human-readable string describing where the human should go to find the source of this resource. "Service ambassador, namespace default, object 1". This is primarly used for diagnostics.kind: the kind of resource this is -- "Mapping", "TLS", "AuthService", whatever.serialization: the original input serialization, if we have it, of the object. If we don't have it, this should beNone-- don't just serialize the object to no purpose.
All of these should be passed as keyword arguments (although it is possible to pass rkey and kind as positional arguments, it is discouraged). A Resource can also accept any other arbitrary keyword arguments, which will be saved in the Resource as they would be in a dict.
Resource defines multiple common methods and mechanisms:
Dot notation and brace notation are equivalent for
Resource:rsrc.fooandrsrc["foo"]are, by definition, equivalent.rsrc.post_error(status: RichStatus)posts an error notification that will be tracked throughout the life of the object. It will be the case that anything trying to use theResourcewill inherit its errors; this is not fully implemented yet.rsrc.referenced_by(other: Resource)marks thisResourceas being referenced by anotherResource. For example, an AmbassadorMappingwill cause Envoy clusters to be created. TheIRClusterobject created to track the cluster will reference theACMappingthat defined the mapping that caused the cluster to be created. This gives the diagnostics service a way to track from the cluster back to the annotation that caused it to exist.rsrc.references(other: Resource)is the other direction: it marks the otherResourceas being referenced by us.rsrc.is_referenced_by(rkey: str) -> Optional[Resource]returnsNoneif the givenrkeyis not associated with aResourcethat references thisResource, or the referencingResourceif it is.rsrc.as_dict() -> dictreturns a raw-dictionary form of just the data fields of theResource. Things like the location and the references table are removed.Class method
Resource.from_dict(rkey: str, location: str, serialization: Optional[str], attrs: dict)creates a newResourceor subclass from a dictionary. Thekindpassed in the dictionary determines the actual class of the object returned (see below for more).Class method
Resource.from_yaml(rkey: str, location: str, serialization: str)deserializes the YAMLserializationand passes that toResource.from_dict.Class method
Resource.from_resource(...)clones aResource, allowing optionally overriding any field using keyword arguments.
The `ACResource` class
ACResource is a subclass of Resource which specifically refers to Ambassador configuration resources. It adds no new behaviors, but two additional keyword arguments are present when initializing:
nameis the name given to this Ambassador resource. Required for every type exceptPragma.apiVersionis the API version to use when interpreting this resource. If not given, it defaults to "ambassador/v0".
Also, ACResource.from_dict will look first for ACResource subclasses when interpreting types.
The `IRResource` class
IRResource is a subclass of Resource which specifically refers to IR resources. IRResource doesn't add any new fields (and, in fact, many IRResource subclasses default some fields) but several new behaviors are added.
File: tools/src/py-mkopensource/README.md
py-mkopensource
py-mkopensource is a program for generating reports of the libraries
used by a Python application, in order to be in compliance with the
attribution requirements of various opensource licenses.
py-mkopensource takes as input the dependencies generated bypip show, and produces a report in either JSON or markdown output.
Running
py-mkopensource requires pip and pip-tools.
To generate the list of dependencies from requirements.txt, run the
following commands:
pip3 --disable-pip-version-check install -r requirements.txt
pip3 --disable-pip-version-check freeze --exclude-editable | cut -d= -f1 | xargs pip show | sed 's/^---$//' >dependenncies.txt
echo '' >>dependenncies.txt
cat dependenncies.txt | ./py-mkopensourceCommand line options
Output type
Parameter --output-type controls the output format.
`--output-type=markdown`
Program outputs dependency information in Markdown format.
This is the default value.
`--output-type=json`
Program outputs dependency information in JSON format
2. Official Technical Reference & Guides (emissary-ingress/emissary-ingress.github.io)
emissary-ingress.dev
The emissary-ingress.dev website, built using Hugo and hosted on Netlify.
Local build
To build and serve the site, you'll need the latest LTS release of Node.
Like Netlify, we use nvm, the Node Version Manager, to install and
manage Node versions:
$ nvm install --ltsYou must also install hugo to build the site.
Setup
- Clone this repo.
- From a terminal window, change to the cloned repo directory.
- Get NPM packages and git submodules, including the the Docsy theme:console
$ npm install
Build or serve the site
To locally serve the site at localhost:1313, run the following command:
$ make run