You can profit from owning stocks when the share price increases, or from quarterly dividend payments. Investments accumulate over time and can yield a solid return due to compound interest, which allows your interest to begin earning interest.
For example, you might make an initial investment of $1,000 and you plan to add $100 every month for 20 years. You'd end up with $75,457.50 after 20 years, even though you only contributed $25,000 over time, if you see an annual return of 10% interest.
Selling Stocks
Knowing when to sell is just as important as buying stocks. Most investors buy when the stock market is rising and sell when it's falling, but a wise investor follows a strategy based on their financial needs.
Keep an eye on the major market indices. The three largest U.S. indices are:
Dow Jones Industrial Average
S&P 500
Nasdaq
Don't panic if they enter a correction or a crash. These events don't tend to last very long, and history has shown that the market will climb again. Losing money is never fun, but it's smart to weather the storm of a down market and hold onto your investments because they may rise again.
Ford stock is just above a 12.14 buy point after previously clearing a three-weeks-tight pattern. It is also rebounding from its 10-week moving average, which also serves as entry.
The RS line has been falling back after a strong spike from early January until mid-March. From a longer term perspective, it has been making progress, with periodic pullbacks, since May 2020. It is now at levels last seen in early 2019.
Ford stock has more than tripled from its 2020 lows. This improving stock market performance has helped its IBD Composite Rating improve to a strong, but not ideal, 86. This puts it in the top 14% of stocks tracked overall.
Earnings are improving, but are lagging its price performance. This is reflected in its EPS Rating of 75. Earnings have accelerated for the two quarters, with EPS growth coming in at 183% in the most recent quarter.
Ford stock has been bolstered by the firm taking a more aggressive stance on investments in electric vehicles and other technology.
Spending in electric and autonomous vehicles will total $29 billion through 2025, more than double prior guidance of $11.5 billion. Of the $29 billion, Ford will spend $22 billion on EVs and $7 billion on autonomy.
The bigger amount even outpaces the $27 billion commitment from rival GM, which had already hiked it from $20 billion.
"We are accelerating all our plans – breaking constraints, increasing battery capacity, improving costs and getting more electric vehicles into our product cycle plan," CEO Jim Farley said when the firm posted earnings in February.
However, the company said chip shortages will hurt production and profit this year. It has had to cut production of its lucrative F-150 pickup due to chip shortages.
The iconic auto giant is ticking boxes for CAN SLIM investors, who look out for firms bringing new products to market.
The Ford Mustang Mach-E launched at the end of 2020 and picked up steam in February. The electric crossover competes with Tesla's Model Y.
Ford also has launched the Mach-E in Europe and has begun production of the Mach-E in China.
It is also an investor in electric truck maker Rivian, whose shares could go public as soon as September and set a record for the biggest EV IPO.
Ford stock rose in February after it announced a six-year partnership with Google parent Alphabet (GOOGL) to develop more connected vehicles. The partnership will put Google apps and services into future Ford and Lincoln vehicles.


You can easily rename your classes, methods and variables with automatic correction of all places where they are used.
To try it, place the caret at the symbol you want to rename, and press &shortcut:RenameElement; (Refactor | Rename). Type the new name in the popup window that appears, or select one of the suggested names, and press Enter.


Please enter a job title.
Please enter a job description.
Please enter a job salary.
Please enter a job duration.


|
JavaTM 2 Platform Standard Ed. 5.0 |
|||||||||
| PREV CLASS NEXT CLASS | FRAMES NO FRAMES | |||||||||
| SUMMARY: NESTED | FIELD | CONSTR | METHOD | DETAIL: FIELD | CONSTR | METHOD |
java.lang.Object java.util.ResourceBundle
public abstract class ResourceBundle
Resource bundles contain locale-specific objects.
When your program needs a locale-specific resource,
a String
for example, your program can load it
from the resource bundle that is appropriate for the
current user's locale. In this way, you can write
program code that is largely independent of the user's
locale isolating most, if not all, of the locale-specific
information in resource bundles.
This allows you to write programs that can:
Resource bundles belong to families whose members share a common base name, but whose names also have additional components that identify their locales. For example, the base name of a family of resource bundles might be "MyResources". The family should have a default resource bundle which simply has the same name as its family - "MyResources" - and will be used as the bundle of last resort if a specific locale is not supported. The family can then provide as many locale-specific members as needed, for example a German one named "MyResources_de".
Each resource bundle in a family contains the same items, but the items have
been translated for the locale represented by that resource bundle.
For example, both "MyResources" and "MyResources_de" may have a
String
that's used on a button for canceling operations.
In "MyResources" the String
may contain "Cancel" and in
"MyResources_de" it may contain "Abbrechen".
If there are different resources for different countries, you can make specializations: for example, "MyResources_de_CH" contains objects for the German language (de) in Switzerland (CH). If you want to only modify some of the resources in the specialization, you can do so.
When your program needs a locale-specific object, it loads
the ResourceBundle
class using the
getBundle
method:
ResourceBundle myResources = ResourceBundle.getBundle("MyResources", currentLocale);
Resource bundles contain key/value pairs. The keys uniquely
identify a locale-specific object in the bundle. Here's an
example of a ListResourceBundle
that contains
two key/value pairs:
Keys are alwayspublic class MyResources extends ListResourceBundle { public Object[][] getContents() { return contents; } static final Object[][] contents = { // LOCALIZE THIS {"OkKey", "OK"}, {"CancelKey", "Cancel"}, // END OF MATERIAL TO LOCALIZE }; }
String
s.
In this example, the keys are "OkKey" and "CancelKey".
In the above example, the values
are also String
s--"OK" and "Cancel"--but
they don't have to be. The values can be any type of object.
You retrieve an object from resource bundle using the appropriate
getter method. Because "OkKey" and "CancelKey"
are both strings, you would use getString
to retrieve them:
The getter methods all require the key as an argument and return the object if found. If the object is not found, the getter method throws abutton1 = new Button(myResources.getString("OkKey")); button2 = new Button(myResources.getString("CancelKey"));
MissingResourceException
.
Besides getString
, ResourceBundle also provides
a method for getting string arrays, getStringArray
,
as well as a generic getObject
method for any other
type of object. When using getObject
, you'll
have to cast the result to the appropriate type. For example:
int[] myIntegers = (int[]) myResources.getObject("intList");
The Java 2 platform provides two subclasses of ResourceBundle
,
ListResourceBundle
and PropertyResourceBundle
,
that provide a fairly simple way to create resources.
As you saw briefly in a previous example, ListResourceBundle
manages its resource as a List of key/value pairs.
PropertyResourceBundle
uses a properties file to manage
its resources.
If ListResourceBundle
or PropertyResourceBundle
do not suit your needs, you can write your own ResourceBundle
subclass. Your subclasses must override two methods: handleGetObject
and getKeys()
.
The following is a very simple example of a ResourceBundle
subclass, MyResources, that manages two resources (for a larger number of
resources you would probably use a Hashtable
).
Notice that you don't need to supply a value if
a "parent-level" ResourceBundle
handles the same
key with the same value (as for the okKey below).
Example:
You do not have to restrict yourself to using a single family of// default (English language, United States) public class MyResources extends ResourceBundle { public Object handleGetObject(String key) { if (key.equals("okKey")) return "Ok"; if (key.equals("cancelKey")) return "Cancel"; return null; } } // German language public class MyResources_de extends MyResources { public Object handleGetObject(String key) { // don't need okKey, since parent level handles it. if (key.equals("cancelKey")) return "Abbrechen"; return null; } }
ResourceBundle
s. For example, you could have a set of bundles for
exception messages, ExceptionResources
(ExceptionResources_fr
, ExceptionResources_de
, ...),
and one for widgets, WidgetResource
(WidgetResources_fr
,
WidgetResources_de
, ...); breaking up the resources however you like.
ListResourceBundle
,
PropertyResourceBundle
,
MissingResourceException
| Field Summary | |
|---|---|
protected ResourceBundle |
parent
The parent bundle of this bundle. |
| Constructor Summary | |
|---|---|
ResourceBundle()
Sole constructor. |
| Method Summary | |
|---|---|
static ResourceBundle |
getBundle(String baseName)
Gets a resource bundle using the specified base name, the default locale, and the caller's class loader. |
static ResourceBundle |
getBundle(String baseName,
Locale locale)
Gets a resource bundle using the specified base name and locale, and the caller's class loader. |
static ResourceBundle |
getBundle(String baseName,
Locale locale,
ClassLoader loader)
Gets a resource bundle using the specified base name, locale, and class loader. |
abstract Enumeration<String> |
getKeys()
Returns an enumeration of the keys. |
Locale |
getLocale()
Returns the locale of this resource bundle. |
Object |
getObject(String key)
Gets an object for the given key from this resource bundle or one of its parents. |
String |
getString(String key)
Gets a string for the given key from this resource bundle or one of its parents. |
String[] |
getStringArray(String key)
Gets a string array for the given key from this resource bundle or one of its parents. |
protected abstract Object |
handleGetObject(String key)
Gets an object for the given key from this resource bundle. |
protected void |
setParent(ResourceBundle parent)
Sets the parent bundle of this bundle. |
| Methods inherited from class java.lang.Object |
|---|
clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait |
| Field Detail |
|---|
protected ResourceBundle parent
getObject
when this bundle does not contain a particular resource.
| Constructor Detail |
|---|
public ResourceBundle()
| Method Detail |
|---|
public final String getString(String key)
(String) getObject
(key)
.
key
- the key for the desired string
NullPointerException
- if key
is null
MissingResourceException
- if no object for the given key can be found
ClassCastException
- if the object found for the given key is not a stringpublic final String[] getStringArray(String key)
(String[]) getObject
(key)
.
key
- the key for the desired string array
NullPointerException
- if key
is null
MissingResourceException
- if no object for the given key can be found
ClassCastException
- if the object found for the given key is not a string arraypublic final Object getObject(String key)
handleGetObject
.
If not successful, and the parent resource bundle is not null,
it calls the parent's getObject
method.
If still not successful, it throws a MissingResourceException.
key
- the key for the desired object
NullPointerException
- if key
is null
MissingResourceException
- if no object for the given key can be foundpublic Locale getLocale()
protected void setParent(ResourceBundle parent)
getObject
when this bundle does not contain a particular resource.
parent
- this bundle's parent bundle.public static final ResourceBundle getBundle(String baseName)
getBundle(baseName, Locale.getDefault(), this.getClass().getClassLoader())
,
except that getClassLoader()
is run with the security
privileges of ResourceBundle
.
See getBundle
for a complete description of the search and instantiation strategy.
baseName
- the base name of the resource bundle, a fully qualified class name
NullPointerException
- if baseName
is null
MissingResourceException
- if no resource bundle for the specified base name can be foundpublic static final ResourceBundle getBundle(String baseName, Locale locale)
getBundle(baseName, locale, this.getClass().getClassLoader())
,
except that getClassLoader()
is run with the security
privileges of ResourceBundle
.
See getBundle
for a complete description of the search and instantiation strategy.
baseName
- the base name of the resource bundle, a fully qualified class namelocale
- the locale for which a resource bundle is desired
NullPointerException
- if baseName
or locale
is null
MissingResourceException
- if no resource bundle for the specified base name can be foundpublic static ResourceBundle getBundle(String baseName, Locale locale, ClassLoader loader)
Conceptually, getBundle
uses the following strategy for locating and instantiating
resource bundles:
getBundle
uses the base name, the specified locale, and the default
locale (obtained from Locale.getDefault
)
to generate a sequence of candidate bundle names.
If the specified locale's language, country, and variant are all empty
strings, then the base name is the only candidate bundle name.
Otherwise, the following sequence is generated from the attribute
values of the specified locale (language1, country1, and variant1)
and of the default locale (language2, country2, and variant2):
Candidate bundle names where the final component is an empty string are omitted. For example, if country1 is an empty string, the second candidate bundle name is omitted.
getBundle
then iterates over the candidate bundle names to find the first
one for which it can instantiate an actual resource bundle. For each candidate
bundle name, it attempts to create a resource bundle:
getBundle
creates a new instance of this class and uses it as the result
resource bundle.
getBundle
attempts to locate a property resource file.
It generates a path name from the candidate bundle name by replacing all "." characters
with "/" and appending the string ".properties".
It attempts to find a "resource" with this name using
ClassLoader.getResource
.
(Note that a "resource" in the sense of getResource
has nothing to do with
the contents of a resource bundle, it is just a container of data, such as a file.)
If it finds a "resource", it attempts to create a new
PropertyResourceBundle
instance from its contents.
If successful, this instance becomes the result resource bundle.
If no result resource bundle has been found, a MissingResourceException
is thrown.
Once a result resource bundle has been found, its parent chain is instantiated.
getBundle
iterates over the candidate bundle names that can be
obtained by successively removing variant, country, and language
(each time with the preceding "_") from the bundle name of the result resource bundle.
As above, candidate bundle names where the final component is an empty string are omitted.
With each of the candidate bundle names it attempts to instantiate a resource bundle, as
described above.
Whenever it succeeds, it calls the previously instantiated resource
bundle's setParent
method
with the new resource bundle, unless the previously instantiated resource
bundle already has a non-null parent.
Implementations of getBundle
may cache instantiated resource bundles
and return the same resource bundle instance multiple times. They may also
vary the sequence in which resource bundles are instantiated as long as the
selection of the result resource bundle and its parent chain are compatible with
the description above.
The baseName
argument should be a fully qualified class name. However, for
compatibility with earlier versions, Sun's Java 2 runtime environments do not verify this,
and so it is possible to access PropertyResourceBundle
s by specifying a
path name (using "/") instead of a fully qualified class name (using ".").
Example: The following class and property files are provided:
MyResources.class, MyResources_fr_CH.properties, MyResources_fr_CH.class,
MyResources_fr.properties, MyResources_en.properties, MyResources_es_ES.class.
The contents of all files are valid (that is, public non-abstract subclasses of ResourceBundle for
the ".class" files, syntactically correct ".properties" files).
The default locale is Locale("en", "GB")
.
Calling getBundle
with the shown locale argument values instantiates
resource bundles from the following sources:
baseName
- the base name of the resource bundle, a fully qualified class namelocale
- the locale for which a resource bundle is desiredloader
- the class loader from which to load the resource bundle
NullPointerException
- if baseName
, locale
, or loader
is null
MissingResourceException
- if no resource bundle for the specified base name can be foundprotected abstract Object handleGetObject(String key)
key
- the key for the desired object
NullPointerException
- if key
is null
public abstract Enumeration<String> getKeys()
|
JavaTM 2 Platform Standard Ed. 5.0 |
|||||||||
| PREV CLASS NEXT CLASS | FRAMES NO FRAMES | |||||||||
| SUMMARY: NESTED | FIELD | CONSTR | METHOD | DETAIL: FIELD | CONSTR | METHOD |
Copyright © 2004, 2010 Oracle and/or its affiliates. All rights reserved. Use is subject to license terms. Also see the documentation redistribution policy.


Welcome
to JavaScript Performance Evaluation suite back| Name | Department | City |
|---|---|---|
| Gabriella | IT | Stockholm |
| Birgit | Marketing | San Francisco |
| Michael | Sales | New York |
| Brad | Management | Abu Dhabi |
| Name | Department | City |
|---|---|---|
| Gabriella | IT | Stockholm |
| Birgit | Marketing | San Francisco |
| Michael | Sales | New York |
| Brad | Management | Abu Dhabi |


Overview
Flight is a lightweight, component-based JavaScript framework that maps behavior to DOM nodes. Twitter uses it for their web applications. By way of example, we've included a simple email client demo (browse the source) built over the Flight framework. There's also a flight implementation over on the todoMVC site (source), courtesy of @mkuklis
Flight uses jQuery and requires a module loader with support for AMD, like WebPack or Require.js. Please read the Flight documentation for installation instructions.
You will have to include jQuery and use a module loader with support for AMD, like Webpack or Require.js Tweet Follow @flightWhy Flight?
Flight is distinct from existing frameworks in that it doesn't prescribe or provide any particular approach to rendering or providing data to a web application. It's agnostic to how requests are routed, which templating language you use or even if you render your HTML on the client or the server. While some web frameworks encourage developers to arrange their code around a prescribed model layer, Flight is organized around the existing DOM model with functionality mapped directly to DOM nodes.
Not only does this obviate the need for additional data structures that will inevitably influence the broader architecture, but by mapping our functionality directly onto the native web we get to take advantage of native features. For example, we get custom event propagation for free by piggy-backing off DOM event bubbling, and our event handling infrastructure works equally well with both native and custom events.
How does it work?
Flight enforces strict separation of concerns. When you create a component you don't get a handle to it. Consequently, components cannot be referenced by other components and cannot become properties of the global object tree. This is by design. Components do not engage each other directly; instead, they broadcast their actions as events which are subscribed to by other components.
Why events?
Events are open ended. When a component triggers an event it has no knowledge of how its request will be satisfied or by whom. This enforced decoupling of functionality allows the engineer to consider each component in isolation rather than having to reason about the growing complexity of the application as a whole.
By making DOM node events proxies for component events, we let the web work for us:
- we get event propagation for free
- a component can subscribe to a given event type at the document level or it can choose to listen only those events originating from within a specified DOM Node
- subscribing components do not distinguish between custom events from other components (e.g.
dataMailItemsServed
) and native DOM node events (e.g.click
), and process both types of event in an identical fashion
Mobility and Testing
Each component is a module, which aside from a minimal set of standard dependencies (relevant Flight utilities and mixins), has no reference to the outside world. Thus a given component will respond to a given event in the same way, regardless of environment. This makes testing both simple and reliable — events are essentially the only variable and a production event is easy to replicate in testing. You can even debug a component by triggering events in the console.
Mixins
A mixin defines a set of functionality that is useful to more than one object. Flight comes with built-in support for functional mixins, including protection against unintentional overrides and duplicate mixins. While classical JavaScript patterns support only single inheritance, a component (or other object) can have multiple mixins applied to it. Moreover mixins requires a fraction of the boilerplate required to form traditional classical hierarchies out of constructor-prototypes hybrids, and don't suffer the leaky abstractions of the latter ('super', 'static', 'const' etc.)
Documentation and Demo
The project includes full documentation as well as an example app in the form of an email client:
Future Work
Flight is an ongoing project. We’re planning to add a full testing framework as well as making available more of the utilities that we use for the Twitter website frontend. We also look forward to your contributions and comments. We know we haven’t thought of everything and with your help we can continue to improve Flight for the benefit of everyone.
We welcome issues and pull requests on GitHub and questions, or assistance to others, on our mailing list.
Acknowledgements
Flight was a group effort.
The following folks contributed to the project: Angus Croll (@angustweets), Dan Webb (@danwrong), Kenneth Kufluk (@kpk) and the members of the Twitter web team.Thanks also to Tom Hamshere (@tbrd) for his work on the README.md and Nicolas Gallagher (@necolas) for general contributions to this site.
On behalf of the Web Core team,
—Angus Croll, Engineer (@angustweets)


FPS:
Time Remaining:
Current Players
Players in Lobby
Waiting to connect to game...
Tournament Scores
Individual players


(but under the hood, the technical implementation is similar) - If you're not familiar with Docker volumes, you can safely ignore this slide! .debug[[k8s/volumes.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/volumes.md)] --- ## Volumes ≠ Persistent Volumes - Volumes and Persistent Volumes are related, but very different! - *Volumes*: - appear in Pod specifications (see next slide) - do not exist as API resources (**cannot** do `kubectl get volumes`) - *Persistent Volumes*: - are API resources (**can** do `kubectl get persistentvolumes`) - correspond to concrete volumes (e.g. on a SAN, EBS, etc.) - cannot be associated with a Pod directly; but through a Persistent Volume Claim - won't be discussed further in this section .debug[[k8s/volumes.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/volumes.md)] --- ## A simple volume example ```yaml apiVersion: v1 kind: Pod metadata: name: nginx-with-volume spec: volumes: - name: www containers: - name: nginx image: nginx volumeMounts: - name: www mountPath: /usr/share/nginx/html/ ``` .debug[[k8s/volumes.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/volumes.md)] --- ## A simple volume example, explained - We define a standalone `Pod` named `nginx-with-volume` - In that pod, there is a volume named `www` - No type is specified, so it will default to `emptyDir` (as the name implies, it will be initialized as an empty directory at pod creation) - In that pod, there is also a container named `nginx` - That container mounts the volume `www` to path `/usr/share/nginx/html/` .debug[[k8s/volumes.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/volumes.md)] --- ## A volume shared between two containers .small[ ```yaml apiVersion: v1 kind: Pod metadata: name: nginx-with-volume spec: volumes: - name: www containers: - name: nginx image: nginx volumeMounts: - name: www mountPath: /usr/share/nginx/html/ - name: git image: alpine command: [ "sh", "-c", "apk add --no-cache git && git clone https://github.com/octocat/Spoon-Knife /www" ] volumeMounts: - name: www mountPath: /www/ restartPolicy: OnFailure ``` ] .debug[[k8s/volumes.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/volumes.md)] --- ## Sharing a volume, explained - We added another container to the pod - That container mounts the `www` volume on a different path (`/www`) - It uses the `alpine` image - When started, it installs `git` and clones the `octocat/Spoon-Knife` repository (that repository contains a tiny HTML website) - As a result, NGINX now serves this website .debug[[k8s/volumes.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/volumes.md)] --- ## Sharing a volume, in action - Let's try it! .exercise[ - Create the pod by applying the YAML file: ```bash kubectl apply -f ~/container.training/k8s/nginx-with-volume.yaml ``` - Check the IP address that was allocated to our pod: ```bash kubectl get pod nginx-with-volume -o wide IP=$(kubectl get pod nginx-with-volume -o json | jq -r .status.podIP) ``` - Access the web server: ```bash curl $IP ``` ] .debug[[k8s/volumes.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/volumes.md)] --- ## The devil is in the details - The default `restartPolicy` is `Always` - This would cause our `git` container to run again ... and again ... and again (with an exponential back-off delay, as explained [in the documentation](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#restart-policy)) - That's why we specified `restartPolicy: OnFailure` - There is a short period of time during which the website is not available (because the `git` container hasn't done its job yet) - This could be avoided by using [Init Containers](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/) (we will see a live example in a few sections) .debug[[k8s/volumes.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/volumes.md)] --- ## Volume lifecycle - The lifecycle of a volume is linked to the pod's lifecycle - This means that a volume is created when the pod is created - This is mostly relevant for `emptyDir` volumes (other volumes, like remote storage, are not "created" but rather "attached" ) - A volume survives across container restarts - A volume is destroyed (or, for remote storage, detached) when the pod is destroyed .debug[[k8s/volumes.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/volumes.md)] --- class: pic .interstitial[![Image separating from the next chapter](https://gallant-turing-d0d520.netlify.com/containers/containers-by-the-water.jpg)] --- name: toc-managing-configuration class: title Managing configuration .nav[ [Previous section](#toc-volumes) | [Back to table of contents](#toc-chapter-4) | [Next section](#toc-managing-stacks-with-helm) ] .debug[(automatically generated title slide)] --- # Managing configuration - Some applications need to be configured (obviously!) - There are many ways for our code to pick up configuration: - command-line arguments - environment variables - configuration files - configuration servers (getting configuration from a database, an API...) - ... and more (because programmers can be very creative!) - How can we do these things with containers and Kubernetes? .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Passing configuration to containers - There are many ways to pass configuration to code running in a container: - baking it into a custom image - command-line arguments - environment variables - injecting configuration files - exposing it over the Kubernetes API - configuration servers - Let's review these different strategies! .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Baking custom images - Put the configuration in the image (it can be in a configuration file, but also `ENV` or `CMD` actions) - It's easy! It's simple! - Unfortunately, it also has downsides: - multiplication of images - different images for dev, staging, prod ... - minor reconfigurations require a whole build/push/pull cycle - Avoid doing it unless you don't have the time to figure out other options .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Command-line arguments - Pass options to `args` array in the container specification - Example ([source](https://github.com/coreos/pods/blob/master/kubernetes.yaml#L29)): ```yaml args: - "--data-dir=/var/lib/etcd" - "--advertise-client-urls=http://<IP_ADDRESS>:2379" - "--listen-client-urls=http://<IP_ADDRESS>:2379" - "--listen-peer-urls=http://<IP_ADDRESS>:2380" - "--name=etcd" ``` - The options can be passed directly to the program that we run ... ... or to a wrapper script that will use them to e.g. generate a config file .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Command-line arguments, pros & cons - Works great when options are passed directly to the running program (otherwise, a wrapper script can work around the issue) - Works great when there aren't too many parameters (to avoid a 20-lines `args` array) - Requires documentation and/or understanding of the underlying program ("which parameters and flags do I need, again?") - Well-suited for mandatory parameters (without default values) - Not ideal when we need to pass a real configuration file anyway .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Environment variables - Pass options through the `env` map in the container specification - Example: ```yaml env: - name: ADMIN_PORT value: "8080" - name: ADMIN_AUTH value: Basic - name: ADMIN_CRED value: "admin:0pensesame!" ``` .warning[`value` must be a string! Make sure that numbers and fancy strings are quoted.] 🤔 Why this weird `{name: xxx, value: yyy}` scheme? It will be revealed soon! .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## The downward API - In the previous example, environment variables have fixed values - We can also use a mechanism called the *downward API* - The downward API allows exposing pod or container information - either through special files (we won't show that for now) - or through environment variables - The value of these environment variables is computed when the container is started - Remember: environment variables won't (can't) change after container start - Let's see a few concrete examples! .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Exposing the pod's namespace ```yaml - name: MY_POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace ``` - Useful to generate FQDN of services (in some contexts, a short name is not enough) - For instance, the two commands should be equivalent: ``` curl api-backend curl api-backend.$MY_POD_NAMESPACE.svc.cluster.local ``` .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Exposing the pod's IP address ```yaml - name: MY_POD_IP valueFrom: fieldRef: fieldPath: status.podIP ``` - Useful if we need to know our IP address (we could also read it from `eth0`, but this is more solid) .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Exposing the container's resource limits ```yaml - name: MY_MEM_LIMIT valueFrom: resourceFieldRef: containerName: test-container resource: limits.memory ``` - Useful for runtimes where memory is garbage collected - Example: the JVM (the memory available to the JVM should be set with the `-Xmx ` flag) - Best practice: set a memory limit, and pass it to the runtime (see [this blog post](https://very-serio.us/2017/12/05/running-jvms-in-kubernetes/) for a detailed example) .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## More about the downward API - [This documentation page](https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/) tells more about these environment variables - And [this one](https://kubernetes.io/docs/tasks/inject-data-application/downward-api-volume-expose-pod-information/) explains the other way to use the downward API (through files that get created in the container filesystem) .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Environment variables, pros and cons - Works great when the running program expects these variables - Works great for optional parameters with reasonable defaults (since the container image can provide these defaults) - Sort of auto-documented (we can see which environment variables are defined in the image, and their values) - Can be (ab)used with longer values ... - ... You *can* put an entire Tomcat configuration file in an environment ... - ... But *should* you? (Do it if you really need to, we're not judging! But we'll see better ways.) .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Injecting configuration files - Sometimes, there is no way around it: we need to inject a full config file - Kubernetes provides a mechanism for that purpose: `configmaps` - A configmap is a Kubernetes resource that exists in a namespace - Conceptually, it's a key/value map (values are arbitrary strings) - We can think about them in (at least) two different ways: - as holding entire configuration file(s) - as holding individual configuration parameters *Note: to hold sensitive information, we can use "Secrets", which are another type of resource behaving very much like configmaps. We'll cover them just after!* .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Configmaps storing entire files - In this case, each key/value pair corresponds to a configuration file - Key = name of the file - Value = content of the file - There can be one key/value pair, or as many as necessary (for complex apps with multiple configuration files) - Examples: ``` # Create a configmap with a single key, "app.conf" kubectl create configmap my-app-config --from-file=app.conf # Create a configmap with a single key, "app.conf" but another file kubectl create configmap my-app-config --from-file=app.conf=app-prod.conf # Create a configmap with multiple keys (one per file in the config.d directory) kubectl create configmap my-app-config --from-file=config.d/ ``` .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Configmaps storing individual parameters - In this case, each key/value pair corresponds to a parameter - Key = name of the parameter - Value = value of the parameter - Examples: ``` # Create a configmap with two keys kubectl create cm my-app-config \ --from-literal=foreground=red \ --from-literal=background=blue # Create a configmap from a file containing key=val pairs kubectl create cm my-app-config \ --from-env-file=app.conf ``` .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Exposing configmaps to containers - Configmaps can be exposed as plain files in the filesystem of a container - this is achieved by declaring a volume and mounting it in the container - this is particularly effective for configmaps containing whole files - Configmaps can be exposed as environment variables in the container - this is achieved with the downward API - this is particularly effective for configmaps containing individual parameters - Let's see how to do both! .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Passing a configuration file with a configmap - We will start a load balancer powered by HAProxy - We will use the [official `haproxy` image](https://hub.docker.com/_/haproxy/) - It expects to find its configuration in `/usr/local/etc/haproxy/haproxy.cfg` - We will provide a simple HAproxy configuration, `k8s/haproxy.cfg` - It listens on port 80, and load balances connections between IBM and Google .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Creating the configmap .exercise[ - Go to the `k8s` directory in the repository: ```bash cd ~/container.training/k8s ``` - Create a configmap named `haproxy` and holding the configuration file: ```bash kubectl create configmap haproxy --from-file=haproxy.cfg ``` - Check what our configmap looks like: ```bash kubectl get configmap haproxy -o yaml ``` ] .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Using the configmap We are going to use the following pod definition: ```yaml apiVersion: v1 kind: Pod metadata: name: haproxy spec: volumes: - name: config configMap: name: haproxy containers: - name: haproxy image: haproxy volumeMounts: - name: config mountPath: /usr/local/etc/haproxy/ ``` .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Using the configmap - The resource definition from the previous slide is in `k8s/haproxy.yaml` .exercise[ - Create the HAProxy pod: ```bash kubectl apply -f ~/container.training/k8s/haproxy.yaml ``` - Check the IP address allocated to the pod: ```bash kubectl get pod haproxy -o wide IP=$(kubectl get pod haproxy -o json | jq -r .status.podIP) ``` ] .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Testing our load balancer - The load balancer will send: - half of the connections to Google - the other half to IBM .exercise[ - Access the load balancer a few times: ```bash curl $IP curl $IP curl $IP ``` ] We should see connections served by Google, and others served by IBM.
(Each server sends us a redirect page. Look at the URL that they send us to!) .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Exposing configmaps with the downward API - We are going to run a Docker registry on a custom port - By default, the registry listens on port 5000 - This can be changed by setting environment variable `REGISTRY_HTTP_ADDR` - We are going to store the port number in a configmap - Then we will expose that configmap as a container environment variable .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Creating the configmap .exercise[ - Our configmap will have a single key, `http.addr`: ```bash kubectl create configmap registry --from-literal=http.addr=<IP_ADDRESS>:80 ``` - Check our configmap: ```bash kubectl get configmap registry -o yaml ``` ] .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Using the configmap We are going to use the following pod definition: ```yaml apiVersion: v1 kind: Pod metadata: name: registry spec: containers: - name: registry image: registry env: - name: REGISTRY_HTTP_ADDR valueFrom: configMapKeyRef: name: registry key: http.addr ``` .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Using the configmap - The resource definition from the previous slide is in `k8s/registry.yaml` .exercise[ - Create the registry pod: ```bash kubectl apply -f ~/container.training/k8s/registry.yaml ``` - Check the IP address allocated to the pod: ```bash kubectl get pod registry -o wide IP=$(kubectl get pod registry -o json | jq -r .status.podIP) ``` - Confirm that the registry is available on port 80: ```bash curl $IP/v2/_catalog ``` ] .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Passwords, tokens, sensitive information - For sensitive information, there is another special resource: *Secrets* - Secrets and Configmaps work almost the same way (we'll expose the differences on the next slide) - The *intent* is different, though: *"You should use secrets for things which are actually secret like API keys, credentials, etc., and use config map for not-secret configuration data."* *"In the future there will likely be some differentiators for secrets like rotation or support for backing the secret API w/ HSMs, etc."* (Source: [the author of both features](https://stackoverflow.com/a/36925553/580281 )) .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- ## Differences between configmaps and secrets - Secrets are base64-encoded when shown with `kubectl get secrets -o yaml` - keep in mind that this is just *encoding*, not *encryption* - it is very easy to [automatically extract and decode secrets](https://medium.com/@mveritym/decoding-kubernetes-secrets-60deed7a96a3) - [Secrets can be encrypted at rest](https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/) - With RBAC, we can authorize a user to access configmaps, but not secrets (since they are two different kinds of resources) .debug[[k8s/configuration.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/configuration.md)] --- class: pic .interstitial[![Image separating from the next chapter](https://gallant-turing-d0d520.netlify.com/containers/distillery-containers.jpg)] --- name: toc-managing-stacks-with-helm class: title Managing stacks with Helm .nav[ [Previous section](#toc-managing-configuration) | [Back to table of contents](#toc-chapter-4) | [Next section](#toc-next-steps) ] .debug[(automatically generated title slide)] --- # Managing stacks with Helm - We created our first resources with `kubectl run`, `kubectl expose` ... - We have also created resources by loading YAML files with `kubectl apply -f` - For larger stacks, managing thousands of lines of YAML is unreasonable - These YAML bundles need to be customized with variable parameters (E.g.: number of replicas, image version to use ...) - It would be nice to have an organized, versioned collection of bundles - It would be nice to be able to upgrade/rollback these bundles carefully - [Helm](https://helm.sh/) is an open source project offering all these things! .debug[[pks/helm.md](https://github.com/paulczar/container.training.git/tree/pks/slides/pks/helm.md)] --- ## Helm concepts - `helm` is a CLI tool - `tiller` is its companion server-side component - A "chart" is an archive containing templatized YAML bundles - Charts are versioned - Charts can be stored on private or public repositories -- *We're going to use the beta of Helm 3 as it does not require `tiller` making things simpler and more secure for us.* .debug[[pks/helm.md](https://github.com/paulczar/container.training.git/tree/pks/slides/pks/helm.md)] --- ## Installing Helm - If the `helm` 3 CLI is not installed in your environment, [install it](https://github.com/helm/helm/releases/tag/v3.0.0-beta.1) .exercise[ - Check if `helm` is installed: ```bash helm version ``` ] -- ```bash version.BuildInfo{Version:"v3.0.0-beta.1", GitCommit:"f76b5f21adb53a85de8925f4a9d4f9bd99f185b5", GitTreeState:"clean", GoVersion:"go1.12.9"}` ``` .debug[[pks/helm.md](https://github.com/paulczar/container.training.git/tree/pks/slides/pks/helm.md)] --- ## Oops you accidently a Helm 2 If `helm version` gives you a result like below it means you have helm 2 which requires the `tiller` server side component. ``` Client: &version.Version{SemVer:"v2.14.0", GitCommit:"05811b84a3f93603dd6c2fcfe57944dfa7ab7fd0", GitTreeState:"clean"} Error: forwarding ports: error upgrading connection: pods "tiller-deploy-6fd87785-x8sxk" is forbidden: User "user1" cannot create resource "pods/portforward" in API group "" in the namespace "kube-system" ``` Run `EXPORT TILLER_NAMESPACE=
` and try again. We've pre-installed `tiller` for you in your namespace just in case. -- Some of the commands in the following may not work in helm 2. Good luck! .debug[[pks/helm.md](https://github.com/paulczar/container.training.git/tree/pks/slides/pks/helm.md)] --- ## Installing Tiller *If you were running Helm 2 you would need to install Tiller. We can skip this.* - Tiller is composed of a *service* and a *deployment* in the `kube-system` namespace - They can be managed (installed, upgraded...) with the `helm` CLI .exercise[ - Deploy Tiller: ```bash helm init ``` ] If Tiller was already installed, don't worry: this won't break it. At the end of the install process, you will see: ``` Happy Helming! ``` .debug[[pks/helm.md](https://github.com/paulczar/container.training.git/tree/pks/slides/pks/helm.md)] --- ## Fix account permissions *If you were running Helm 2 you would need to install Tiller. We can skip this.* - Helm permission model requires us to tweak permissions - In a more realistic deployment, you might create per-user or per-team service accounts, roles, and role bindings .exercise[ - Grant `cluster-admin` role to `kube-system:default` service account: ```bash kubectl create clusterrolebinding add-on-cluster-admin \ --clusterrole=cluster-admin --serviceaccount=kube-system:default ``` ] (Defining the exact roles and permissions on your cluster requires a deeper knowledge of Kubernetes' RBAC model. The command above is fine for personal and development clusters.) .debug[[pks/helm.md](https://github.com/paulczar/container.training.git/tree/pks/slides/pks/helm.md)] --- ## View available charts - A public repo is pre-configured when installing Helm - We can view available charts with `helm search` (and an optional keyword) .exercise[ - View all available charts: ```bash helm search hub ``` - View charts related to `prometheus`: ```bash helm search hub prometheus ``` ] .debug[[pks/helm.md](https://github.com/paulczar/container.training.git/tree/pks/slides/pks/helm.md)] --- ## Add the stable chart repository - Helm 3 does not come configured with any repositories, so we need to start by adding the stable repo. .exercise[ - Add the stable repo ```bash helm repo add stable https://kubernetes-charts.storage.googleapis.com/ helm repo update ``` ] .debug[[pks/helm.md](https://github.com/paulczar/container.training.git/tree/pks/slides/pks/helm.md)] --- ## Install a chart - Most charts use `LoadBalancer` service types by default - Most charts require persistent volumes to store data - We can relax these requirements a bit .exercise[ - Install on our cluster: ```bash helm install wp stable/wordpress \ --set service.type=ClusterIP \ --set persistence.enabled=false \ --set mariadb.master.persistence.enabled=false ``` ] Where do these `--set` options come from? .debug[[pks/helm.md](https://github.com/paulczar/container.training.git/tree/pks/slides/pks/helm.md)] --- ## Inspecting a chart - `helm inspect` shows details about a chart (including available options) .exercise[ - See the metadata and all available options for `stable/wordpress`: ```bash helm inspect stable/wordpress ``` ] The chart's metadata includes a URL to the project's home page. (Sometimes it conveniently points to the documentation for the chart.) .debug[[pks/helm.md](https://github.com/paulczar/container.training.git/tree/pks/slides/pks/helm.md)] --- ## Viewing installed charts - Helm keeps track of what we've installed .exercise[ - List installed Helm charts: ```bash helm list ``` ] .debug[[pks/helm.md](https://github.com/paulczar/container.training.git/tree/pks/slides/pks/helm.md)] --- ## Why wordpress its 2019?!?! I know ... funny right :) .debug[[pks/helm-wordpress.md](https://github.com/paulczar/container.training.git/tree/pks/slides/pks/helm-wordpress.md)] --- ## Helm install notes - You'll notice a helpful message after running `helm install` that looks something like this: ``` NOTES: 1. Get the WordPress URL: echo "WordPress URL: http://<IP_ADDRESS>:8080/" echo "WordPress Admin URL: http://<IP_ADDRESS>:8080/admin" kubectl port-forward --namespace user1 svc/wp-wordpress 8080:80 2. Login with the following credentials to see your blog echo Username: user echo Password: $(kubectl get secret --namespace user1 wp-wordpress -o jsonpath="{.data.wordpress-password}" | base64 --decode) ``` -- Helm charts generally have a `NOTES.txt` template that is rendered out and displayed after helm commands are run. Pretty neat. .debug[[pks/helm-wordpress.md](https://github.com/paulczar/container.training.git/tree/pks/slides/pks/helm-wordpress.md)] --- ## What did helm install ? - Run `kubectl get all` to check what resources helm installed .exercise[ - Run `kubectl get all`: ```bash kubectl get all ``` ] .debug[[pks/helm-wordpress.md](https://github.com/paulczar/container.training.git/tree/pks/slides/pks/helm-wordpress.md)] --- ## What did helm install ? ``` NAME READY STATUS RESTARTS AGE pod/wp-mariadb-0 1/1 Running 0 11m pod/wp-wordpress-6cb9cfc94-chbr6 1/1 Running 0 11m NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/wp-mariadb ClusterIP <IP_ADDRESS>
3306/TCP 11m service/wp-wordpress ClusterIP <IP_ADDRESS>
80/TCP,443/TCP 11m NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/wp-wordpress 1/1 1 1 11m NAME DESIRED CURRENT READY AGE replicaset.apps/tiller-deploy-6487f7bfd8 1 1 1 2d6h replicaset.apps/tiller-deploy-75ccf68856 0 0 0 2d6h replicaset.apps/wp-wordpress-6cb9cfc94 1 1 1 11m NAME READY AGE statefulset.apps/wp-mariadb 1/1 11m ``` .debug[[pks/helm-wordpress.md](https://github.com/paulczar/container.training.git/tree/pks/slides/pks/helm-wordpress.md)] --- ## Check if wordpress is working - Using the notes provided from helm check you can access your wordpress and login as `user` .exercise[ - run the commands provided by the helm summary: ```bash echo Username: user echo Password: $(kubectl get secret --namespace user1 wp-wordpress -o jsonpath="{.data.wordpress-password}" | base64 --decode) kubectl port-forward --namespace user1 svc/wp-wordpress 8080:80 ``` ] -- Yay? you have a 2003 era blog .debug[[pks/helm-wordpress.md](https://github.com/paulczar/container.training.git/tree/pks/slides/pks/helm-wordpress.md)] --- ## Helm Chart Values Settings values on the command line is okay for a demonstration, but we should really be creating a `~/workshop/values.yaml` file for our chart. Let's do that now. > the values file is a bit long to copy/paste from here, so lets wget it. .exercise[ - Download the values.yaml file and edit it, changing the URL prefix to be `
-wp`: ```bash wget -O ~/workshop/values.yaml \ https://raw.githubusercontent.com/paulczar/container.training/pks/slides/pks/wp/values.yaml vim ~/workshop/values.yaml helm upgrade wp stable/wordpress -f ~/workshop/values.yaml ``` ] --- .debug[[pks/helm-wordpress.md](https://github.com/paulczar/container.training.git/tree/pks/slides/pks/helm-wordpress.md)] --- class: pic .interstitial[![Image separating from the next chapter](https://gallant-turing-d0d520.netlify.com/containers/lots-of-containers.jpg)] --- name: toc-next-steps class: title Next steps .nav[ [Previous section](#toc-managing-stacks-with-helm) | [Back to table of contents](#toc-chapter-5) | [Next section](#toc-links-and-resources) ] .debug[(automatically generated title slide)] --- # Next steps *Alright, how do I get started and containerize my apps?* -- Suggested containerization checklist: .checklist[ - write a Dockerfile for one service in one app - write Dockerfiles for the other (buildable) services - write a Compose file for that whole app - make sure that devs are empowered to run the app in containers - set up automated builds of container images from the code repo - set up a CI pipeline using these container images - set up a CD pipeline (for staging/QA) using these images ] And *then* it is time to look at orchestration! .debug[[k8s/whatsnext.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/whatsnext.md)] --- ## Options for our first production cluster - Get a managed cluster from a major cloud provider (AKS, EKS, GKE...) (price: $, difficulty: medium) - Hire someone to deploy it for us (price: $$, difficulty: easy) - Do it ourselves (price: $-$$$, difficulty: hard) .debug[[k8s/whatsnext.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/whatsnext.md)] --- ## One big cluster vs. multiple small ones - Yes, it is possible to have prod+dev in a single cluster (and implement good isolation and security with RBAC, network policies...) - But it is not a good idea to do that for our first deployment - Start with a production cluster + at least a test cluster - Implement and check RBAC and isolation on the test cluster (e.g. deploy multiple test versions side-by-side) - Make sure that all our devs have usable dev clusters (whether it's a local minikube or a full-blown multi-node cluster) .debug[[k8s/whatsnext.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/whatsnext.md)] --- ## Namespaces - Namespaces let you run multiple identical stacks side by side - Two namespaces (e.g. `blue` and `green`) can each have their own `redis` service - Each of the two `redis` services has its own `ClusterIP` - CoreDNS creates two entries, mapping to these two `ClusterIP` addresses: `redis.blue.svc.cluster.local` and `redis.green.svc.cluster.local` - Pods in the `blue` namespace get a *search suffix* of `blue.svc.cluster.local` - As a result, resolving `redis` from a pod in the `blue` namespace yields the "local" `redis` .warning[This does not provide *isolation*! That would be the job of network policies.] .debug[[k8s/whatsnext.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/whatsnext.md)] --- ## Relevant sections - [Namespaces](kube-selfpaced.yml.html#toc-namespaces) - [Network Policies](kube-selfpaced.yml.html#toc-network-policies) - [Role-Based Access Control](kube-selfpaced.yml.html#toc-authentication-and-authorization) (covers permissions model, user and service accounts management ...) .debug[[k8s/whatsnext.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/whatsnext.md)] --- ## Stateful services (databases etc.) - As a first step, it is wiser to keep stateful services *outside* of the cluster - Exposing them to pods can be done with multiple solutions: - `ExternalName` services
(`redis.blue.svc.cluster.local` will be a `CNAME` record) - `ClusterIP` services with explicit `Endpoints`
(instead of letting Kubernetes generate the endpoints from a selector) - Ambassador services
(application-level proxies that can provide credentials injection and more) .debug[[k8s/whatsnext.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/whatsnext.md)] --- ## Stateful services (second take) - If we want to host stateful services on Kubernetes, we can use: - a storage provider - persistent volumes, persistent volume claims - stateful sets - Good questions to ask: - what's the *operational cost* of running this service ourselves? - what do we gain by deploying this stateful service on Kubernetes? - Relevant sections: [Volumes](kube-selfpaced.yml.html#toc-volumes) | [Stateful Sets](kube-selfpaced.yml.html#toc-stateful-sets) | [Persistent Volumes](kube-selfpaced.yml.html#toc-highly-available-persistent-volumes) - Excellent [blog post](http://www.databasesoup.com/2018/07/should-i-run-postgres-on-kubernetes.html) tackling the question: “Should I run Postgres on Kubernetes?” .debug[[k8s/whatsnext.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/whatsnext.md)] --- ## HTTP traffic handling - *Services* are layer 4 constructs - HTTP is a layer 7 protocol - It is handled by *ingresses* (a different resource kind) - *Ingresses* allow: - virtual host routing - session stickiness - URI mapping - and much more! - [This section](kube-selfpaced.yml.html#toc-exposing-http-services-with-ingress-resources) shows how to expose multiple HTTP apps using [Træfik](https://docs.traefik.io/user-guide/kubernetes/) .debug[[k8s/whatsnext.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/whatsnext.md)] --- ## Logging - Logging is delegated to the container engine - Logs are exposed through the API - Logs are also accessible through local files (`/var/log/containers`) - Log shipping to a central platform is usually done through these files (e.g. with an agent bind-mounting the log directory) - [This section](kube-selfpaced.yml.html#toc-centralized-logging) shows how to do that with [Fluentd](https://docs.fluentd.org/v0.12/articles/kubernetes-fluentd) and the EFK stack .debug[[k8s/whatsnext.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/whatsnext.md)] --- ## Metrics - The kubelet embeds [cAdvisor](https://github.com/google/cadvisor), which exposes container metrics (cAdvisor might be separated in the future for more flexibility) - It is a good idea to start with [Prometheus](https://prometheus.io/) (even if you end up using something else) - Starting from Kubernetes 1.8, we can use the [Metrics API](https://kubernetes.io/docs/tasks/debug-application-cluster/core-metrics-pipeline/) - [Heapster](https://github.com/kubernetes/heapster) was a popular add-on (but is being [deprecated](https://github.com/kubernetes/heapster/blob/master/docs/deprecation.md) starting with Kubernetes 1.11) .debug[[k8s/whatsnext.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/whatsnext.md)] --- ## Managing the configuration of our applications - Two constructs are particularly useful: secrets and config maps - They allow to expose arbitrary information to our containers - **Avoid** storing configuration in container images (There are some exceptions to that rule, but it's generally a Bad Idea) - **Never** store sensitive information in container images (It's the container equivalent of the password on a post-it note on your screen) - [This section](kube-selfpaced.yml.html#toc-managing-configuration) shows how to manage app config with config maps (among others) .debug[[k8s/whatsnext.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/whatsnext.md)] --- ## Managing stack deployments - The best deployment tool will vary, depending on: - the size and complexity of your stack(s) - how often you change it (i.e. add/remove components) - the size and skills of your team - A few examples: - shell scripts invoking `kubectl` - YAML resources descriptions committed to a repo - [Helm](https://github.com/kubernetes/helm) (~package manager) - [Spinnaker](https://www.spinnaker.io/) (Netflix' CD platform) - [Brigade](https://brigade.sh/) (event-driven scripting; no YAML) .debug[[k8s/whatsnext.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/whatsnext.md)] --- ## Cluster federation -- ![Star Trek Federation](images/startrek-federation.jpg) -- Sorry Star Trek fans, this is not the federation you're looking for! -- (If I add "Your cluster is in another federation" I might get a 3rd fandom wincing!) .debug[[k8s/whatsnext.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/whatsnext.md)] --- ## Cluster federation - Kubernetes master operation relies on etcd - etcd uses the [Raft](https://raft.github.io/) protocol - Raft recommends low latency between nodes - What if our cluster spreads to multiple regions? -- - Break it down in local clusters - Regroup them in a *cluster federation* - Synchronize resources across clusters - Discover resources across clusters .debug[[k8s/whatsnext.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/whatsnext.md)] --- ## Developer experience *We've put this last, but it's pretty important!* - How do you on-board a new developer? - What do they need to install to get a dev stack? - How does a code change make it from dev to prod? - How does someone add a component to a stack? .debug[[k8s/whatsnext.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/whatsnext.md)] --- class: pic .interstitial[![Image separating from the next chapter](https://gallant-turing-d0d520.netlify.com/containers/plastic-containers.JPG)] --- name: toc-links-and-resources class: title Links and resources .nav[ [Previous section](#toc-next-steps) | [Back to table of contents](#toc-chapter-5) | [Next section](#toc-) ] .debug[(automatically generated title slide)] --- # Links and resources All things Kubernetes: - [Kubernetes Community](https://kubernetes.io/community/) - Slack, Google Groups, meetups - [Kubernetes on StackOverflow](https://stackoverflow.com/questions/tagged/kubernetes) - [Play With Kubernetes Hands-On Labs](https://medium.com/@marcosnils/introducing-pwk-play-with-k8s-159fcfeb787b) All things Docker: - [Docker documentation](http://docs.docker.com/) - [Docker Hub](https://hub.docker.com) - [Docker on StackOverflow](https://stackoverflow.com/questions/tagged/docker) - [Play With Docker Hands-On Labs](http://training.play-with-docker.com/) Everything else: - [Local meetups](https://www.meetup.com/) .footnote[These slides (and future updates) are on → http://container.training/] .debug[[k8s/links.md](https://github.com/paulczar/container.training.git/tree/pks/slides/k8s/links.md)] --- class: title, self-paced Thank you! .debug[[shared/thankyou.md](https://github.com/paulczar/container.training.git/tree/pks/slides/shared/thankyou.md)] --- class: title, in-person That's all, folks!
Questions? ![end](images/end.jpg) .debug[[shared/thankyou.md](https://github.com/paulczar/container.training.git/tree/pks/slides/shared/thankyou.md)]


EASTER 2 FRIDAY
Entrance Antiphon
You have redeemed us, Lord, by your blood, from every tribe and tongue and people and nation,
and have made us a kingdom and priests for our God, alleluia.
 Through our Lord...
FIRST READING Acts 5:34-42
A reading from the Acts of the Apostles
In those days: A Pharisee in the council named
Gamaliel, a teacher of the law, held in honour by all the people, stood up and
ordered the apostles to be put outside for a while. And he said to them,
"Men of Israel, take care what you do with these
men. For before these days Theudas arose, giving
himself out to be somebody, and a number of men, about four hundred, joined
him; but he was slain and all who followed him were dispersed and came to
nothing. After him Judas the Galilean arose in the days of the census and drew
away some of the people after him; he also perished, and all who followed him
were scattered. So in the present case I tell you,
keep away from these men and let them alone; for if this plan or this
undertaking is of men, it will fail; but if it is of God, you will not be able
to overthrow them. You might even be found opposing God!" So they took his
advice, and when they had called in the apostles, they beat them and charged
them not to speak in the name of Jesus, and let them go. Then they left the
presence of the council, rejoicing that they were counted worthy to suffer
dishonour for the name. And every day in the temple and at home they did not
cease teaching and preaching Jesus as the Christ.
The word of the Lord
RESPONSORIAL PSALM Psalm 27
R. There is one thing I ask of the Lord, to live in the house of the Lord.
Or: Alleluia!
The LORD is my light and my salvation; whom shall I
fear? The LORD is the stronghold of my life; whom should I dread?
R. There is one thing I ask of the Lord, to live in
the house of the Lord.
There is one thing I ask of the LORD, only this do I
seek: to live in the house of the LORD all the days of my life, to gaze on the beauty
of the LORD, to inquire at his temple.
R. There is one thing I ask of the Lord, to live in
the house of the Lord.
I believe I shall see the LORD's
goodness in the land of the living. Wait for the LORD; be strong; be stouthearted, and wait for the LORD!
R. There is one thing I ask of the Lord, to live in
the house of the Lord.
ALLELUIA Matthew 4:4b
R. Alleluia. Man shall not live by bread alone, but by every word that proceeds from the mouth of God. R. Alleluia.
Gospel John 6:1-15
A reading from the holy Gospel according to John
At that time: Jesus went to the other side of the
Sea of Galilee, which is the
The Gospel of the Lord
Prayer over the Offerings
Accept in compassion, Lord, we pray, the offerings of your family, that under your protective care they may never lose what they have received,
but attain the gifts that are eternal. Through Christ our Lord.
Communion Antiphon
Christ our Lord was handed over for our transgressions, and was raised again for our justification, alleluia.
Prayer after Communion
Keep safe, O Lord, we pray, those whom you have saved by your kindness, that,
redeemed by the Passion of your Son, they may rejoice in his Resurrection.
Who lives and reigns for ever and ever.


forge.embyr.deprecated.raw module¶
-
class
forge.embyr.deprecated.raw.
MyServerProtocol
¶ Bases:
autobahn.twisted.websocket.WebSocketServerProtocol
-
beginMessage
(isBinary=False, doNotCompress=False)¶ Implements
autobahn.websocket.interfaces.IWebSocketChannel.beginMessage()
-
beginMessageFrame
(length)¶ Implements
autobahn.websocket.interfaces.IWebSocketChannel.beginMessageFrame()
-
connectionLost
(reason)¶ Called when the connection is shut down.
Clear any circular references here, and any external references to this Protocol. The connection has been closed.
@type reason: L{twisted.python.failure.Failure}
-
connectionMade
()¶ Called when a connection is made.
This may be considered the initializer of the protocol, because it is called when the connection is completed. For clients, this is called once the connection to the server has been established; for servers, this is called after an accept() call stops blocking and a socket has been received. If you need to send any greeting or initial message, do it here.
-
consumeData
()¶ Consume buffered (incoming) data.
-
dataReceived
(data)¶ Called whenever data is received.
Use this method to translate to a higher-level message. Usually, some callback will be made upon the receipt of each complete protocol message.
- @param data: a string of indeterminate length. Please keep in mind
that you will probably need to buffer some data, as partial (or multiple) protocol messages may be received! I recommend that unit tests for protocols call through to this method with differing chunk sizes, down to one byte at a time.
-
dropConnection
(abort=False)¶ Drop the underlying TCP connection.
-
endMessage
()¶ Implements
autobahn.websocket.interfaces.IWebSocketChannel.endMessage()
-
failHandshake
(reason, code=400, responseHeaders=None)¶ During opening handshake the client request was invalid, we send a HTTP error response and then drop the connection.
-
get_channel_id
(channel_id_type='tls-unique')¶ Implements
autobahn.wamp.interfaces.ITransport.get_channel_id()
-
logPrefix
()¶ Return a prefix matching the class name, to identify log messages related to this protocol instance.
-
logRxFrame
(frameHeader, payload)¶ Hook fired right after WebSocket frame has been received and decoded, but only when self.logFrames == True.
-
logRxOctets
(data)¶ Hook fired right after raw octets have been received, but only when self.logOctets == True.
-
logTxFrame
(frameHeader, payload, repeatLength, chopsize, sync)¶ Hook fired right after WebSocket frame has been encoded and sent, but only when self.logFrames == True.
-
logTxOctets
(data, sync)¶ Hook fired right after raw octets have been sent, but only when self.logOctets == True.
-
makeConnection
(transport)¶ Make a connection to a transport and a server.
This sets the ‘transport’ attribute of this Protocol, and calls the connectionMade() callback.
-
onAutoPingTimeout
()¶ When doing automatic ping/pongs to detect broken connection, the peer did not reply in time to our ping. We drop the connection.
-
onClose
(wasClean, code, reason)¶ Implements
autobahn.websocket.interfaces.IWebSocketChannel.onClose()
-
onCloseFrame
(code, reasonRaw)¶ Callback when a Close frame was received. The default implementation answers by sending a Close when no Close was sent before. Otherwise it drops the TCP connection either immediately (when we are a server) or after a timeout (when we are a client and expect the server to drop the TCP).
- Parameters
code (int) – Close status code, if there was one (
WebSocketProtocol
.CLOSE_STATUS_CODE_*).reasonRaw (bytes) – Close reason (when present, a status code MUST have been also be present).
-
onCloseHandshakeTimeout
()¶ We expected the peer to respond to us initiating a close handshake. It didn’t respond (in time self.closeHandshakeTimeout) with a close response frame though. So we drop the connection, but set self.wasClean = False.
-
onConnect
(request)¶ Callback fired during WebSocket opening handshake when new WebSocket client connection is about to be established.
When you want to accept the connection, return the accepted protocol from list of WebSocket (sub)protocols provided by client or None to speak no specific one or when the client protocol list was empty.
You may also return a pair of (protocol, headers) to send additional HTTP headers, with headers being a dictionary of key-values.
Throw
autobahn.websocket.types.ConnectionDeny
when you don’t want to accept the WebSocket connection request.- Parameters
request (instance of
autobahn.websocket.protocol.ConnectionRequest
) – WebSocket connection request information.
-
onFrameBegin
()¶ Begin of receive new frame.
-
onFrameData
(payload)¶ New data received within frame.
-
onFrameEnd
()¶ End of frame received.
-
onMessage
(payload, isBinary)¶ Implements
autobahn.websocket.interfaces.IWebSocketChannel.onMessage()
-
onMessageBegin
(isBinary)¶ Implements
autobahn.websocket.interfaces.IWebSocketChannel.onMessageBegin()
-
onMessageEnd
()¶ Implements
autobahn.websocket.interfaces.IWebSocketChannel.onMessageEnd()
-
onMessageFrame
(payload)¶ Implements
autobahn.websocket.interfaces.IWebSocketChannel.onMessageFrame()
-
onMessageFrameBegin
(length)¶ Implements
autobahn.websocket.interfaces.IWebSocketChannel.onMessageFrameBegin()
-
onMessageFrameData
(payload)¶ Implements
autobahn.websocket.interfaces.IWebSocketChannel.onMessageFrameData()
-
onMessageFrameEnd
()¶ Implements
autobahn.websocket.interfaces.IWebSocketChannel.onMessageFrameEnd()
-
onOpen
()¶ Implements
autobahn.websocket.interfaces.IWebSocketChannel.onOpen()
-
onOpenHandshakeTimeout
()¶ We expected the peer to complete the opening handshake with to us. It didn’t do so (in time self.openHandshakeTimeout). So we drop the connection, but set self.wasClean = False.
-
onPing
(payload)¶ Implements
autobahn.websocket.interfaces.IWebSocketChannel.onPing()
-
onPong
(payload)¶ Implements
autobahn.websocket.interfaces.IWebSocketChannel.onPong()
-
onServerConnectionDropTimeout
()¶ We (a client) expected the peer (a server) to drop the connection, but it didn’t (in time self.serverConnectionDropTimeout). So we drop the connection, but set self.wasClean = False.
-
processControlFrame
()¶ Process a completely received control frame.
-
processData
()¶ After WebSocket handshake has been completed, this procedure will do all subsequent processing of incoming bytes.
-
processHandshake
()¶ Process WebSocket opening handshake request from client.
-
processProxyConnect
()¶ Process proxy connect.
-
registerProducer
(producer, streaming)¶ Register a Twisted producer with this protocol.
- Parameters
producer (object) – A Twisted push or pull producer.
streaming (bool) – Producer type.
-
sendClose
(code=None, reason=None)¶ Implements
autobahn.websocket.interfaces.IWebSocketChannel.sendClose()
-
sendCloseFrame
(code=None, reasonUtf8=None, isReply=False)¶ Send a close frame and update protocol state. Note, that this is an internal method which deliberately allows not send close frame with invalid payload.
-
sendData
(data, sync=False, chopsize=None)¶ Wrapper for self.transport.write which allows to give a chopsize. When asked to chop up writing to TCP stream, we write only chopsize octets and then give up control to select() in underlying reactor so that bytes get onto wire immediately. Note that this is different from and unrelated to WebSocket data message fragmentation. Note that this is also different from the TcpNoDelay option which can be set on the socket.
-
sendFrame
(opcode, payload=b'', fin=True, rsv=0, mask=None, payload_len=None, chopsize=None, sync=False)¶ Send out frame. Normally only used internally via sendMessage(), sendPing(), sendPong() and sendClose().
This method deliberately allows to send invalid frames (that is frames invalid per-se, or frames invalid because of protocol state). Other than in fuzzing servers, calling methods will ensure that no invalid frames are sent.
In addition, this method supports explicit specification of payload length. When payload_len is given, it will always write that many octets to the stream. It’ll wrap within payload, resending parts of that when more octets were requested The use case is again for fuzzing server which want to sent increasing amounts of payload data to peers without having to construct potentially large messages themselves.
-
sendHtml
(html)¶ Send HTML page HTTP response.
-
sendHttpErrorResponse
(code, reason, responseHeaders=None)¶ Send out HTTP error response.
-
sendMessage
(payload, isBinary=False, fragmentSize=None, sync=False, doNotCompress=False)¶ Implements
autobahn.websocket.interfaces.IWebSocketChannel.sendMessage()
-
sendMessageFrame
(payload, sync=False)¶ Implements
autobahn.websocket.interfaces.IWebSocketChannel.sendMessageFrame()
-
sendMessageFrameData
(payload, sync=False)¶ Implements
autobahn.websocket.interfaces.IWebSocketChannel.sendMessageFrameData()
-
sendPing
(payload=None)¶ Implements
autobahn.websocket.interfaces.IWebSocketChannel.sendPing()
-
sendPong
(payload=None)¶ Implements
autobahn.websocket.interfaces.IWebSocketChannel.sendPong()
-
sendPreparedMessage
(preparedMsg)¶ Implements
autobahn.websocket.interfaces.IWebSocketChannel.sendPreparedMessage()
-
sendRedirect
(url)¶ Send HTTP Redirect (303) response.
-
sendServerStatus
(redirectUrl=None, redirectAfter=0)¶ Used to send out server status/version upon receiving a HTTP/GET without upgrade to WebSocket header (and option serverStatus is True).
-
setTrackTimings
(enable)¶ Enable/disable tracking of detailed timings.
- Parameters
enable (bool) – Turn time tracking on/off.
-
succeedHandshake
(res)¶ Callback after onConnect() returns successfully. Generates the response for the handshake.
-
unregisterProducer
()¶ Unregister Twisted producer with this protocol.
-


Greens
Installation
Overview
Greens is an interactive device portraying a sci-fi and technology-laden ecosystem. It discusses the interdependent, inter-oppressed but intertwined relationship among humans, nature, and technology. Via extracting mutable features from the environment and transferring user’s voice data, Greens creates a system that makes invisible data from our environment visible and makes society aware of the impact of nature.
Role
Interaction Designer
Hardware Fabrication, VUI Design, Prototyping
July 2019 - November 2019
Background
Over the last few decades, the exploitation of our planet and the degradation of our environment has gone up at an alarming rate. Our living planet Earth is currently facing a lot of environmental concerns. The environmental problems like global warming, acid rain, air pollution, urban sprawl, waste disposal, ozone layer depletion, water pollution, climate change and many more affect every human, animal, and nation on this planet.
Design Process
Concept
In our lives, there are all kinds of environmental data, which are important indicators and messages in the ecosystem. However, these data are invisible, so it is difficult for people to perceive its existence and changes, and ignore that our daily choices are closely related to the drastic changes in the environment. Human behaviors may sometimes inevitably change the surrounding environment, and moreover, would cause different degree of problems.
How might we effectively use this data and make social impact ? Let people see, feel, or hear these intangible data more deeply and further raise environmental awareness.
Details
We designed an interactive device — Greens with environmentally friendly and recyclable materials as the main material. The core of this interactive device includes: voice user interface, tangible user interface, and experience design.
Greens provide a process of fluid experience by serial voice inputs, entailing the outputs of jazzy AI music, sci-fi projecting images, and futuristic mechanical ecosystem model.
Implementation
The installation mainly uses recyclable materials and uses laser cutting to complete most of the assembly components. The design style is based on technology, geometry, and abstract appearance.
Voice User Interface
We use Google Assistant to build our voice-based interface. Google Assistant SDK is set up on the dev board, and the voice interface is built with Dialogflow with testing in Action On Google.
Shape Changing TUI
We design a few paper models and paper sculptures, they represent different roles in the sci-fi ecosystem. Arduino control system would receive the voice inputs and activate different motors in our mechanical device, which contains a lot of interactive elements of different shapes. These objects are composed of different paper materials and cardboard-based mechanism interfaces.
Music Generation & Mapping
Users’ voice inputs enable the Coral TPU board to trigger the pygame module to play the auto-generated jazz music produced by Tensorflow and pymidi module, and also would change our projecting images controlled by Madmapper.
Demostration
The interactive dialogue is divided into four rounds. The voice interface is used to record people's carbon footprint, and the data is converted into automatically generated music and tangible objects. Through interactive installation - Greens, it is expected that the designed experience can arouse people's awareness of environmental protection.
Let's go green together!
Link
Award
ACM Uist SIC 2019
Jury’s Choice Award (Honorable Mention)
People’s Choice Award


public class World
extends java.lang.Object
This class also allows us to construct any map for the game world.
The World class has functions to display the nearby rooms in printed format.
| Constructor and Description |
|---|
World(Player gamePlayer)
Constructor for objects of class World.
|
| Modifier and Type | Method and Description |
|---|---|
int |
getNumRooms()
Allows us to get the total number of rooms in the map.
|
static Room |
getRoom(int row,
int column)
Retrive a room in the map at a specified location
|
static Room |
getRoom(java.lang.String name)
Retrive a room in the map by its name
|
java.lang.String |
getRoomDetails(Room room)
Gives the details about a room.
|
static void |
placeItemsInRooms()
Go through the items list and place each item into the room associated with that item.
|
void |
printNearbyRoomsMap()
Print out the nearbyRoomsMap, line by line.
|
static boolean |
roomExists(int row,
int column)
Search through our map to find a specific room
|
public World(Player gamePlayer)
gamePlayer
- The player in the game.public int getNumRooms()
public static boolean roomExists(int row, int column)
row
- row of the room that we're trying to find.column
- column of the room that we're trying to find.public static Room getRoom(int row, int column)
row
- row of the room being queriedcolumn
- column of the room being queriedpublic static Room getRoom(java.lang.String name)
name
- the name of the room being searched for.public java.lang.String getRoomDetails(Room room)
room
- a reference to the room that the player is seeking details about.public static void placeItemsInRooms()
public void printNearbyRoomsMap()


PemaMVC was inspired by CorMVC is a jQuery-powered Model-View-Controller (MVC) framework
Click on the contacts tab to see an example.
Please fill out the contact information below.
Please fill out the contact information below.
Sorry, but the page you requested could not be found.
Try going back to the home page.


Fluid with intrinsic ratio. Overwrite the ratio in your own CSS.
Fluid with fixed height, centered both vertically und horizontally. Overwrite the height in your own CSS.


Features:
Options:
Overview
Build your own Line Array speakers with a set of these comprehensive and detailed plans.
Sprague Audio Works designs cost-effective, scalable professional audio loudspeakers systems for working pro-sound folks on a budget.
Because you construct the boxes you get big name performance at do-it-yourself prices. Sprague Audio Works designs are proven performers.
We've done all the engineering, all you have to do is make some saw dust and assemble them!
Check out the sample plans
Scalability
We've all seen guys buy loudspeaker systems that are attempts at "one size fits all." And if you've set up enough shows, you know it's more like, "one size fits none!" Because Sprague Audio Works systems are designed to be used in multiples, your system can be just right for the job at hand. Small gigs can be handled with one or two light weight cabinets over a pair of subs. Have a bigger show? No sweat! The LA112 line array rigging system can accommodate up to 8 cabinets. Add as many super efficient SAW-118s as you need to create an earthquake of first octave reinforcement from 35Hz up! Whatever job you have, you can take just the right amount of lightweight, stackable gear.For smaller venues or when great music and vocal quality is required at lower volumes in churches and smaller auditoriums, the LA206 is a great match - using dual 6.5" drivers with a 1.4" exit compression driver and a proven HF waveguide designed specifically for line arrays.
And new for 2016, is the LA215 - a large format dual 15" 3 way system for the ultimate a high performance speaker system at a fraction of the cost of most commercial systems of this caliber. The LA215 incorprates a dual 1.4" exit waveguide that has been tighly integrated into the top and bottom cabinet panels allowing very tight and cohesive coupling of the HF section when stacked.
A new addition to the product line is the LA803 which is built around the new Eminence Alpha 3 drivers. This cabinet is easy to build and provides surprisingly articulate sound at volumes perfect for smaller venues. This is designed to couple with the new SAW112 subwoofer. which makes for a very compact yet high performace system.