mirror of
https://github.com/clearlinux/rkt.git
synced 2026-06-16 02:05:48 +00:00
*: move spec to new home
The appc spec has moved! Its new home is at https://github.com/appc/spec This removes the spec (since it has already been migrated), and adds placeholders linking to the new repo. It also updates rocket to reference the spec repo _directly_ (i.e. NOT godepped). This means that people will need to maintain the spec in their gopath for now when building rocket.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
The following guide will show you how to build and run a self-contained Go app
|
||||
using rocket, the reference implementation of the [App Container
|
||||
Specification](https://github.com/coreos/rocket/tree/master/app-container).
|
||||
Specification](https://github.com/appc/spec).
|
||||
|
||||
## Create a hello go application
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ $ find /var/lib/rkt/cas/blob/
|
||||
/var/lib/rkt/cas/blob/sha256/70/sha256-f9215c18b86f406c7cec4c7b45fd8752b5bfd1a492507d647821c2ce593fbf31
|
||||
```
|
||||
|
||||
Per the App Container [spec](app-container/SPEC.md#image-archives) the SHA-256 is of the tarball, which is reproducible with other tools:
|
||||
Per the App Container [spec](https://github.com/appc/spec/blob/master/SPEC.md#image-archives) the SHA-256 is of the tarball, which is reproducible with other tools:
|
||||
|
||||
```
|
||||
$ wget https://github.com/coreos/etcd/releases/download/v0.5.0-alpha.4/etcd-v0.5.0-alpha.4-linux-amd64.aci
|
||||
@@ -81,9 +81,9 @@ The escape character ```^]``` is generated by ```Ctrl-]``` on a US keyboard, on
|
||||
|
||||
## App Container basics
|
||||
|
||||
[App Container](app-container) is a [specification](app-container/SPEC.md) of an image format, runtime, and discovery protocol for running a container. We anticipate app container to be adopted by other runtimes outside of Rocket itself. Read more about it [here](app-container).
|
||||
[App Container][appc-repo] is a [specification][appc-spec] of an image format, runtime, and discovery protocol for running a container. We anticipate app container to be adopted by other runtimes outside of Rocket itself. Read more about it [here][appc-repo].
|
||||
|
||||
To validate the `rkt` with the App Container [validation ACIs](app-container/README.md) run:
|
||||
To validate the `rkt` with the App Container [validation ACIs][appc-readme] run:
|
||||
|
||||
```
|
||||
$ rkt run -volume database:/tmp \
|
||||
@@ -91,6 +91,10 @@ $ rkt run -volume database:/tmp \
|
||||
https://github.com/coreos/rocket/releases/download/v0.1.0/ace-validator-sidekick.aci
|
||||
```
|
||||
|
||||
[appc-repo]: https://github.com/appc/spec/
|
||||
[appc-spec]: https://github.com/appc/spec/blob/master/SPEC.md
|
||||
[appc-readme]: https://github.com/appc/spec/blob/master/README.md
|
||||
|
||||
## Rocket internals
|
||||
|
||||
Rocket is designed to be modular and pluggable by default. To do this we have a concept of "stages" of execution of the container.
|
||||
|
||||
+2
-185
@@ -1,186 +1,3 @@
|
||||
# App Container
|
||||
# App Container
|
||||
|
||||
## Overview
|
||||
|
||||
This repository contains schema definitions and tools for the App Container specification.
|
||||
See [SPEC.md](SPEC.md) for details of the specification itself.
|
||||
|
||||
_Thank you to Tobi Knaup and Ben Hindman from Mesosphere, and the Pivotal Engineering team for providing initial feedback on the spec._
|
||||
|
||||
- `schema` contains JSON definitions of the different constituent formats of the spec (the _App Manifest_, the _Container Runtime Manifest_, and the `Fileset Manifest`). These JSON schemas also handle validation of the manifests through their Marshal/Unmarshal implementations.
|
||||
- `schema/types` contains various types used by the Manifest types to enforce validation
|
||||
- `ace` contains a tool intended to be run within an _Application Container Executor_ to validate that the ACE has set up the container environment correctly. This tool can be built into an ACI image ready for running on an executor by using the `build_aci` script.
|
||||
- `actool` contains a tool for building and validating images and manifests that meet the App Container specifications.
|
||||
|
||||
## Building ACIs
|
||||
|
||||
`actool` can be used to build an Application Container Image from an application root filesystem (rootfs). It currently supports two modes: building an ACI from an existing [app manifest](SPEC.md#app-manifest), or building a [fileset image](SPEC.md#fileset-images) from a rootfs alone.
|
||||
|
||||
For example, to build a fileset containing certificate authorities, one could do the following:
|
||||
```
|
||||
$ actool build --fileset-name ca-certs /tmp/ca-certs/ ca-certs.aci
|
||||
$ echo $?
|
||||
0
|
||||
```
|
||||
|
||||
Since an ACI is simply an (optionally compressed) tar file, we can inspect the created file with simple tools:
|
||||
|
||||
```
|
||||
$ tar tvf ca-certs.aci
|
||||
drwxrwxr-x 1000/1000 0 2014-01-02 03:04 rootfs/
|
||||
drwxrwxr-x 1000/1000 0 2014-01-02 03:04 rootfs/certs/
|
||||
-rw-rw-r-- 1000/1000 3140 2014-01-02 03:04 rootfs/certs/ca-bundle.crt
|
||||
-rw-rw-r-- 1000/1000 1581 2014-01-02 03:04 rootfs/certs/example.com.crt
|
||||
-rw-r-xr-x root/root 174 2014-01-02 03:04 fileset
|
||||
$ tar xf ca-certs.aci fileset -O | python -m json.tool
|
||||
{
|
||||
"acKind": "FilesetManifest",
|
||||
"acVersion": "0.1.0",
|
||||
"arch": "amd64",
|
||||
"dependencies": null,
|
||||
"files": [
|
||||
"/certs/",
|
||||
"/ca-bundle.crt",
|
||||
"/example.com.crt",
|
||||
],
|
||||
"name": "ca-certs",
|
||||
"os": "linux"
|
||||
}
|
||||
```
|
||||
|
||||
To build an ACI image containing an application, supply a valid app manifest and the rootfs:
|
||||
|
||||
```
|
||||
$ actool build --app-manifest my-app.json my_app/rootfs my-app.aci
|
||||
```
|
||||
|
||||
Again, examining the ACI is simple, as is verifying that the app manifest was embedded appropriately:
|
||||
```
|
||||
$ tar tvf ca-certs.aci
|
||||
drwxrwxr-x 1000/1000 0 2014-01-02 03:04 rootfs/
|
||||
-rw-rw-r-- 1000/1000 1581 2014-01-02 03:04 rootfs/my_app
|
||||
-rw-r-xr-x root/root 174 2014-01-02 03:04 app
|
||||
```
|
||||
|
||||
```
|
||||
$ tar xf my-app.aci app -O | python -m json.tool
|
||||
{
|
||||
"acKind": "AppManifest",
|
||||
"acVersion": "1.0.0",
|
||||
"arch": "amd64",
|
||||
"exec": [
|
||||
"/my_app",
|
||||
],
|
||||
"group": "0",
|
||||
"name": "my_app",
|
||||
"os": "linux",
|
||||
"user": "0"
|
||||
}
|
||||
```
|
||||
|
||||
## Validating App Container implementations
|
||||
|
||||
`actool validate` can be used by implementations of the App Container Specification to check that files they produce conform to the expectations.
|
||||
|
||||
### Validating App Manifests, Fileset Manifests and Container Runtime Manifests
|
||||
|
||||
To validate one of the three manifest types in the specification, simply run `actool validate` against the file.
|
||||
|
||||
```
|
||||
$ actool validate ./app.json
|
||||
./app.json: valid AppManifest
|
||||
$ echo $?
|
||||
0
|
||||
```
|
||||
|
||||
Multiple arguments are supported, and the output can be silenced with `-quiet`:
|
||||
|
||||
```
|
||||
$ actool validate app1.json app2.json
|
||||
app1.json: valid AppManifest
|
||||
app2.json: valid AppManifest
|
||||
$ actool -quiet validate app2.json
|
||||
$ echo $?
|
||||
0
|
||||
```
|
||||
|
||||
`actool` will automatically determine which type of manifest it is checking (by using the `acKind` field common to all manifests), so there is no need to specify which type of manifest is being validated:
|
||||
```
|
||||
$ actool validate /tmp/my_fileset
|
||||
/tmp/my_fileset: valid FilesetManifest
|
||||
```
|
||||
|
||||
If a manifest fails validation the first error encountered is returned along with a non-zero exit status:
|
||||
```
|
||||
$ actool validate nover.json
|
||||
nover.json: invalid AppManifest: acVersion must be set
|
||||
$ echo $?
|
||||
1
|
||||
```
|
||||
|
||||
### Validating ACIs and layouts
|
||||
|
||||
Validating ACIs or layouts is very similar to validating manifests: simply run the `actool validate` subcommmand directly against an image or directory, and it will determine the type automatically:
|
||||
```
|
||||
$ actool validate app.aci
|
||||
app.aci: valid app container image
|
||||
```
|
||||
|
||||
```
|
||||
$ actool validate app_layout/
|
||||
app_layout/: valid image layout
|
||||
```
|
||||
|
||||
To override the type detection and force `actool validate` to validate as a particular type (image, layout or manifest), use the `--type` flag:
|
||||
|
||||
```
|
||||
actool validate -type appimage hello.aci
|
||||
hello.aci: valid app container image
|
||||
```
|
||||
|
||||
### Validating App Container Executors (ACEs)
|
||||
|
||||
The [`ace`](ace/) package contains a simple go application, the _ACE validator_, which can be used to validate app container executors by checking certain expectations about the environment in which it is run: for example, that the appropriate environment variables and mount points are set up as defined in the specification.
|
||||
|
||||
To use the ACE validator, first compile it into an ACI using the supplied `build_aci` script:
|
||||
```
|
||||
$ app-container/ace/build_aci
|
||||
|
||||
You need a passphrase to unlock the secret key for
|
||||
user: "Joe Bloggs (Example, Inc) <joe@example.com>"
|
||||
4096-bit RSA key, ID E14237FD, created 2014-03-31
|
||||
|
||||
Wrote main layout to bin/ace_main_layout
|
||||
Wrote unsigned main ACI bin/ace_validator_main.aci
|
||||
Wrote main layout hash bin/sha256-f7eb89d44f44d416f2872e43bc5a4c6c3e12c460e845753e0a7b28cdce0e89d2
|
||||
Wrote main ACI signature bin/ace_validator_main.sig
|
||||
|
||||
You need a passphrase to unlock the secret key for
|
||||
user: "Joe Bloggs (Example, Inc) <joe@example.com>"
|
||||
4096-bit RSA key, ID E14237FD, created 2014-03-31
|
||||
|
||||
Wrote sidekick layout to bin/ace_sidekick_layout
|
||||
Wrote unsigned sidekick ACI bin/ace_validator_sidekick.aci
|
||||
Wrote sidekick layout hash bin/sha256-13b5598069dbf245391cc12a71e0dbe8f8cdba672072135ebc97948baacf30b2
|
||||
Wrote sidekick ACI signature bin/ace_validator_sidekick.sig
|
||||
|
||||
```
|
||||
|
||||
As can be seen, the script generates two ACIs: `ace_validator_main.aci`, the main entrypoint to the validator, and `ace_validator_sidekick.aci`, a sidekick application. The sidekick is used to validate that an ACE implementation properly handles running multiple applications in a container (for example, that they share a mount namespace), and hence both ACIs should be run together in a layout to validate proper ACE behaviour. The script also generates detached signatures which can be verified by the ACE.
|
||||
|
||||
When running the ACE validator, output is minimal if tests pass, and errors are reported as they occur - for example:
|
||||
|
||||
```
|
||||
preStart OK
|
||||
main OK
|
||||
sidekick OK
|
||||
postStop OK
|
||||
```
|
||||
|
||||
or, on failure:
|
||||
```
|
||||
main FAIL
|
||||
==> file "/prestart" does not exist as expected
|
||||
==> unexpected environment variable "WINDOWID" set
|
||||
==> timed out waiting for /db/sidekick
|
||||
```
|
||||
The App Container repository has moved to https://github.com/appc/spec/
|
||||
|
||||
+1
-586
@@ -1,588 +1,3 @@
|
||||
# App Container Specification
|
||||
|
||||
The "App Container" defines an image format, image discovery mechanism and execution environment that can exist in several independent implementations. The core goals include:
|
||||
|
||||
* Design for fast downloads and starts of the containers
|
||||
* Ensure images are cryptographically verifiable and highly-cacheable
|
||||
* Design for composability and independent implementations
|
||||
* Use common technologies for crypto, archive, compression and transport
|
||||
* Use the DNS namespace to name and discover container images
|
||||
|
||||
To achieve these goals this specification is split out into a number of smaller sections.
|
||||
|
||||
1. The **[App Container Image](#app-container-image)** defines: how files are assembled together into a single image, verified on download and placed onto disk to be run.
|
||||
|
||||
2. The **[App Container Executor](#app-container-executor)** defines: how an app container image on disk is run and the environment it is run inside including cgroups, namespaces and networking.
|
||||
|
||||
* The [Metadata Server](#app-container-metadata-service) defines how a container can introspect and get a cryptographically verifiable identity from the execution environment.
|
||||
|
||||
3. The **[App Container Image Discovery](#app-container-image-discovery)** defines: how to take a name like example.com/reduce-worker and translate that into a downloadable image.
|
||||
|
||||
|
||||
## Example Use Case
|
||||
|
||||
To provide context to the specs outlined below we will walk through an example.
|
||||
|
||||
A user wants to launch a container running two processes.
|
||||
The two processes the user wants to run are the apps named `example.com/reduce-worker-register` and `example.com/reduce-worker`.
|
||||
First, the executor will check the cache and find that it doesn't have images available for these apps.
|
||||
So, it will make an HTTPS request to example.com and using the `<meta>` tags there finds that the containers can be found at:
|
||||
|
||||
https://storage-mirror.example.com/reduce-worker-register.aci
|
||||
https://storage-mirror.example.com/reduce-worker.aci
|
||||
|
||||
The executor downloads these two images and puts them into its local on-disk cache.
|
||||
Then the executor extracts two fresh copies of the images to create instances of the "on-disk app format" and reads the two app manifests to figure out what binaries will need to be executed.
|
||||
|
||||
Based on user input the executor now sets up the necessary cgroups, network interfaces, etc and forks the `register` and `reduce-worker` processes in their shared namespaces inside the container.
|
||||
|
||||
At some point, the container will get some notification that it needs to stop.
|
||||
The executor will send `SIGTERM` to the processes and after they have exited the `post-stop` event handlers for each app will run.
|
||||
|
||||
Now, let's dive into the pieces that took us from two URLs to a running container on our system.
|
||||
|
||||
## App Container Image
|
||||
|
||||
An *App Container Image* (ACI) contains all files and metadata needed to execute a given app.
|
||||
In some ways you can think of an ACI as equivalent to a static binary.
|
||||
This file layout must be followed for the app to be executed by an Executor.
|
||||
|
||||
### Image Layout
|
||||
|
||||
The on-disk layout of an app container is straightforward.
|
||||
It includes a *rootfs* with all of the files that will exist in the root of the app and an *app image manifest* describing the contents of the image and how to execute the app.
|
||||
|
||||
```
|
||||
/manifest
|
||||
/rootfs
|
||||
/rootfs/usr/bin/data-downloader
|
||||
/rootfs/usr/bin/reduce-worker
|
||||
```
|
||||
|
||||
### Image Archives
|
||||
|
||||
The ACI archive format aims for flexibility and relies on very boring technologies: HTTP, gpg, tar and gzip.
|
||||
This set of formats makes it easy to build, host and secure a container using technologies that are battle tested.
|
||||
|
||||
Images archives MUST be a tar formatted file.
|
||||
The image may be optionally compressed with gzip, bzip2 or xz.
|
||||
After compression images may also be encrypted with AES symmetric encryption.
|
||||
|
||||
```
|
||||
tar cvvf reduce-worker.tar app rootfs
|
||||
gpg --output reduce-worker.sig --detach-sig reduce-worker.tar
|
||||
gzip reduce-worker.tar -c > reduce-worker.aci
|
||||
```
|
||||
|
||||
Optional encryption:
|
||||
|
||||
```
|
||||
gpg --output reduce-worker.aci --digest-algo sha256 --cipher-algo AES256 --symmetric reduce-worker.aci
|
||||
```
|
||||
|
||||
All files in the image must maintain all of their original properties including: timestamps, Unix modes and extended attributes (xattrs).
|
||||
|
||||
An image is addressed and verified against the hash of its uncompressed tar file, the _image ID_.
|
||||
The default digest format is sha256, but all hash IDs in this format are prefixed by the algorithm used (e.g. sha256-a83...).
|
||||
|
||||
```
|
||||
echo sha256-$(sha256sum reduce-worker.tar |awk '{print $1}')
|
||||
```
|
||||
|
||||
**Note**: the key distribution mechanism is not defined here.
|
||||
Implementations of the app container spec will need to provide a mechanism for users to configure the list of signing keys to trust or use the key discovery described in "App Container Image Discovery".
|
||||
|
||||
Example application container image builder: **TODO** link to actool
|
||||
|
||||
### App Image Manifest
|
||||
|
||||
The [app image manifest](#app-image-manifest-schema) is a JSON file that includes details about the contents of the app image, and optionally information about how to execute a process inside the app image's rootfs.
|
||||
If included, execution details include mount points that should exist, the user, the command args, default cgroup settings and more.
|
||||
The manifest may also define binaries to execute in response to lifecycle events of the main process such as *pre-start* and *post-stop*.
|
||||
|
||||
App manifests MAY specify dependencies, which describe how to assemble the final rootfs from a collection of other images.
|
||||
As an example, you might have an app that needs special certificates layered into its filesystem.
|
||||
In this case, you can reference the name "example.com/trusted-certificate-authority" as a dependency in the app image manifest.
|
||||
The dependencies are applied in order and each app image dependency can overwrite files from the previous dependency.
|
||||
An optional path whitelist can be used to omit certain files from dependencies being included in the final assembled rootfs.
|
||||
|
||||
Image Format TODO
|
||||
|
||||
* Define the garbage collection lifecycle of the container filesystem including:
|
||||
* Format of app exit code and signal
|
||||
* The refcounting plan for resources consumed by the ACE such as volumes
|
||||
* Define the lifecycle of the container as all exit or first to exit
|
||||
* Define security requirements for a container. In particular is any isolation of users required between containers? What user does each application run under and can this be root (i.e. "real" root in the host).
|
||||
* Define how apps are supposed to communicate; can they/do they 'see' each other (a section in the apps perspective would help)?
|
||||
|
||||
|
||||
## App Container Executor
|
||||
|
||||
App Containers are a combination of a number of technologies which are not aware of each other.
|
||||
This specification attempts to define a reasonable subset of steps to accomplish a few goals:
|
||||
|
||||
* Creating a filesystem hierarchy in which the app will execute
|
||||
* Running the app process inside of a combination of resource and namespace isolations
|
||||
* Executing the application inside of this environment
|
||||
|
||||
There are two "perspectives" in this process.
|
||||
The "*executor*" perspective consists of the steps that the container executor must take to set up the containers. The "*app*" perspective is how the app processes inside the container see the environment.
|
||||
|
||||
### Executor Perspective
|
||||
|
||||
#### Filesystem Setup
|
||||
|
||||
Every execution of an app container should start from a clean copy of the app image.
|
||||
The simplest implementation will take an application container image and extract it into a new directory:
|
||||
|
||||
```
|
||||
cd $(mktemp -d -t temp.XXXX)
|
||||
mkdir hello
|
||||
tar xzvf /var/lib/pce/hello.aci -C hello
|
||||
```
|
||||
|
||||
Other implementations could increase performance and de-duplicate data by building on top of overlay filesystems, copy-on-write block devices, or a content-addressed file store.
|
||||
These details are orthogonal to the runtime environment.
|
||||
|
||||
#### Container Runtime Manifest
|
||||
|
||||
A container executes one or more apps with shared PID namespace, network namespace, mount namespace, IPC namespace and UTS namespace.
|
||||
Each app will start pivoted (i.e. chrooted) into its own unique read-write rootfs before execution.
|
||||
The definition of the container is a list of apps that should be launched together, along with isolators that should apply to the entire container.
|
||||
This is codified in a [Container Runtime Manifest](#container-runtime-manifest-schema).
|
||||
|
||||
This example container will use a set of three apps:
|
||||
|
||||
| Name | Version | Image hash |
|
||||
|------------------------------------|---------|-------------------------------------------------|
|
||||
| example.com/reduce-worker | 1.0.0 | sha256-277205b3ae3eb3a8e042a62ae46934b470e431ac |
|
||||
| example.com/worker-backup | 1.0.0 | sha256-3e86b59982e49066c5d813af1c2e2579cbf573de |
|
||||
| example.com/reduce-worker-register | 1.0.0 | sha256-86298e1fdb95ec9a45b5935504e26ec29b8feffa |
|
||||
|
||||
#### Volume Setup
|
||||
|
||||
Volumes that are specified in the Container Runtime Manifest are mounted into each of the apps via a bind mount.
|
||||
For example say that the worker-backup and reduce-worker both have a MountPoint named "work".
|
||||
In this case, the container executor will bind mount the host's `/opt/tenant1/database` directory into the Path of each of the matching "work" MountPoints of the two containers.
|
||||
|
||||
#### Network Setup
|
||||
|
||||
An App Container must have a [layer 3](http://en.wikipedia.org/wiki/Network_layer) (commonly called the IP layer) network interface; this can be instantiated in any number of ways (e.g. veth, macvlan, ipvlan, device pass-through).
|
||||
The network interface should be configured with an IPv4/IPv6 address that is reachable from other containers.
|
||||
|
||||
#### Logging
|
||||
|
||||
Apps should log to stdout and stderr. The container executor is responsible for capturing and persisting the output.
|
||||
|
||||
If the application detects other logging options, such as the /run/systemd/system/journal socket, it may optionally upgrade to using those mechanisms.
|
||||
Note that logging mechanisms other than stdout and stderr are not required by this specification (or tested by the compliance tests).
|
||||
|
||||
### Apps Perspective
|
||||
|
||||
#### Execution Environment
|
||||
|
||||
* **Working directory** always the root of the application image
|
||||
* **PATH** `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`
|
||||
* **USER, LOGNAME** username of the user executing this app
|
||||
* **HOME** home directory of the user
|
||||
* **SHELL** login shell of the user
|
||||
* **AC_APP_NAME** name of the application (as defined in the app manifest)
|
||||
* **AC_METADATA_URL** URL that the metadata service for this container can be found
|
||||
|
||||
### Isolators
|
||||
|
||||
Isolators enforce resource constraints rather than namespacing.
|
||||
Isolators may be applied to individual applications, to whole containers, or to both.
|
||||
Some well known isolators can be verified by the specification.
|
||||
Additional isolators will be added to this specification over time.
|
||||
|
||||
|Name|Type|Schema|Example|
|
||||
|-------------------------|------|------------------------------------|----------------|
|
||||
|cpu/shares/ |string|"<uint>" |"4096" |
|
||||
|memory/limit |string|"<bytes>" |"1G", "5T", "4K"|
|
||||
|blockIO/readBandwidth |string|"<path to file> <bytes>"|"/tmp 1K" |
|
||||
|blockIO/writeBandwidth |string|"<path to file> <bytes>"|"/tmp 1K" |
|
||||
|networkIO/readBandwidth |string|"<device name> <bytes>" |"eth0 100M" |
|
||||
|networkIO/writeBandwidth |string|"<device name> <bytes>" |"eth0 100M" |
|
||||
|privateNetwork |string|"<true|false>" |"true" |
|
||||
|capabilities/boundingSet |string|"<cap> <cap> ..." |"CAP_NET_BIND_SERVICE CAP_SYS_ADMIN"|
|
||||
|
||||
#### Types
|
||||
|
||||
* uint: base 10 formatted unsigned int as a string
|
||||
* bytes: Suffix to a base 10 int to make it a K, M, G, or T for base 1024
|
||||
|
||||
## App Container Image Discovery
|
||||
|
||||
An app name has a URL-like structure, for example `example.com/reduce-worker`.
|
||||
However, there is no scheme on this app name so we can't directly resolve it to an app container image URL.
|
||||
Furthermore, attributes other than the name may be required to unambiguously identify an app (version, OS and architecture).
|
||||
App Container Image Discovery prescribes a discovery process to retrieve an image based on the app name and these attributes.
|
||||
|
||||
### Simple Discovery
|
||||
|
||||
First, try to fetch the app container image by rendering the following template and directly retrieving the resulting URL:
|
||||
|
||||
https://{name}-{version}-{os}-{arch}.aci
|
||||
|
||||
For example, given the app name `example.com/reduce-worker`, with version `1.0.0`, arch `amd64`, and os `linux`, try to retrieve:
|
||||
|
||||
https://example.com/reduce-worker-1.0.0-linux-amd64.aci
|
||||
|
||||
If this fails, move on to meta discovery.
|
||||
If this succeeds, try fetching the signature using the same template but with a `.sig` extension:
|
||||
|
||||
https://example.com/reduce-worker-1.0.0-linux-amd64.sig
|
||||
|
||||
### Meta Discovery
|
||||
|
||||
If simple discovery fails, then we use HTTPS+HTML meta tags to resolve an app name to a downloadable URL.
|
||||
For example, if the ACE is looking for `example.com/reduce-worker` it will request:
|
||||
|
||||
https://example.com/reduce-worker?ac-discovery=1
|
||||
|
||||
Then inspect the HTML returned for meta tags that have the following format:
|
||||
|
||||
```
|
||||
<meta name="ac-discovery" content="prefix-match url-tmpl">
|
||||
<meta name="ac-discovery-pubkeys" content="prefix-match url">
|
||||
```
|
||||
|
||||
* `ac-discovery` should contain a URL template that can be rendered to retrieve the app image or signature
|
||||
* `ac-discovery-pubkeys` should contain a URL that provides a set of public keys that can be used to verify the signature of the app image
|
||||
|
||||
Some examples for different schemes and URLs:
|
||||
|
||||
```
|
||||
<meta name="ac-discovery" content="example.com https://storage.example.com/{os}/{arch}/{name}-{version}.{ext}?torrent">
|
||||
<meta name="ac-discovery" content="example.com hdfs://storage.example.com/{name}-{version}-{os}-{arch}.{ext}">
|
||||
<meta name="ac-discovery-pubkeys" content="example.com https://example.com/pubkeys.gpg">
|
||||
```
|
||||
|
||||
The algorithm first ensures that the prefix of the AC Name matches the prefix-match and then if there is a match it will request the equivalent of:
|
||||
|
||||
```
|
||||
curl $(echo "$urltmpl" | sed -e "s/{name}/$appname/" -e "s/{version}/$version/ -e "s/{os}/$os/" -e "s/{arch}/$arch/" -e "s/{ext}/$ext/")
|
||||
```
|
||||
|
||||
where _appname_, _version_, _os_, and _arch_ are set to their respective values for the application, and _ext_ is either `aci` or `sig` for retrieving an app image or signature respectively.
|
||||
|
||||
In our example above this would be:
|
||||
|
||||
```
|
||||
sig: https://storage.example.com/linux/amd64/reduce-worker-1.0.0.sig
|
||||
aci: https://storage.example.com/linux/amd64/reduce-worker-1.0.0.aci
|
||||
keys: https://example.com/pubkeys.gpg
|
||||
```
|
||||
|
||||
This mechanism is only used for discovery of contents URLs.
|
||||
Anything implementing this spec should enforce any signing rules set in place by the operator and ensure the app manifest provided by the fetched app image are all prefixed from the same domain.
|
||||
|
||||
Discovery URLs that require interpolation are [RFC6570](https://tools.ietf.org/html/rfc6570) URI templates.
|
||||
|
||||
Inspired by: https://golang.org/cmd/go/#hdr-Remote_import_paths
|
||||
|
||||
## App Container Metadata Service
|
||||
|
||||
For a variety of reasons, it is desirable to not write files to the filesystem in order to run a container:
|
||||
* Secrets can be kept outside of the container (such as the identity endpoint specified below)
|
||||
* Writing files leads to assumptions like a libc environment attempting parse `/etc/hosts`
|
||||
* The container can be run on top of a cryptographically secured read-only filesystem
|
||||
* Metadata is a proven system for virtual machines
|
||||
|
||||
The app container specification defines an HTTP-based metadata service for providing metadata to containers.
|
||||
|
||||
### Metadata Server
|
||||
|
||||
The ACE must provide a Metadata server on the address given to the container via the `AC_METADATA_URL` environment variable.
|
||||
By convention, the default address will be `http://169.254.169.255`.
|
||||
|
||||
Clients querying any of these endpoints must specify the `Metadata-Flavor: AppContainer` header.
|
||||
|
||||
### Container Metadata
|
||||
|
||||
Information about the container that this app is executing in.
|
||||
|
||||
Retrievable at `http://$AC_METADATA_URL/acMetadata/v1/container`
|
||||
|
||||
| Entry | Description |
|
||||
|-------------|-------------|
|
||||
|annotations/ | A directory of metadata values passed to the container.|
|
||||
|manifest | The container manifest JSON |
|
||||
|uid | The unique execution container uid.|
|
||||
|
||||
### App Metadata
|
||||
|
||||
Every running process will be able to introspect its App Name via the `AC_APP_NAME` environment variable.
|
||||
This is necessary to query for the correct endpoint metadata.
|
||||
|
||||
Retrievable at `http://$AC_METADATA_URL/acMetadata/v1/apps/${ac_app_name}/`
|
||||
|
||||
| Entry | Description |
|
||||
|---------------|-------------|
|
||||
|annotations/ | A directory of metadata values on the entrypoint manifest.|
|
||||
|image/manifest | The original manifest file of the app. |
|
||||
|image/id | Cryptographic image ID this app is on.|
|
||||
|
||||
### Identity Endpoint
|
||||
|
||||
As a basic building block for building a secure identity system, the metadata service must provide an HMAC (described in [RFC2104](https://www.ietf.org/rfc/rfc2104.txt)) endpoint for use by the apps in the container.
|
||||
This gives a cryptographically verifiable identity to the container based on its container unique ID and the container HMAC key, which is held securely by the ACE.
|
||||
|
||||
Accessible at `http://169.254.169.255/acMetadata/v1/container/hmac`
|
||||
|
||||
| Entry | Description |
|
||||
|-------|-------------|
|
||||
|sign | POST any object to this endpoint and retrieve a base64 hmac-sha256 signature as the response body. The metadata service holds onto the AES key as a sort of container TPM.|
|
||||
|verify | Verify a signature from another container. POST a form with signature=<base64 encoded signature> and uid=<uid of the container that generated the signature>. Returns 200 OK if the signature passes. |
|
||||
|
||||
|
||||
## AC Name Type
|
||||
|
||||
An AC Name Type is restricted to lowercase characters accepted by the DNS [RFC](http://tools.ietf.org/html/rfc1123#page-13) and "/".
|
||||
|
||||
Examples:
|
||||
|
||||
* database
|
||||
* example.com/database
|
||||
* example.com/ourapp
|
||||
* sub-domain.example.com/org/product/release
|
||||
|
||||
An AC Name Type cannot be an empty string.
|
||||
The AC Name Type is used as the primary key for a number of fields in the schemas below.
|
||||
The schema validator will ensure that the keys conform to these constraints.
|
||||
|
||||
|
||||
## Manifest Schemas
|
||||
|
||||
### App Image Manifest Schema
|
||||
|
||||
JSON Schema for the App Image Manifest
|
||||
|
||||
```
|
||||
{
|
||||
"acKind": "ImageManifest",
|
||||
"acVersion": "0.1.0",
|
||||
"name": "example.com/reduce-worker",
|
||||
"labels": [
|
||||
{
|
||||
"name": "version",
|
||||
"val": "1.0.0"
|
||||
},
|
||||
{
|
||||
"name": "arch",
|
||||
"val": "amd64"
|
||||
},
|
||||
{
|
||||
"name": "os",
|
||||
"val": "linux"
|
||||
}
|
||||
],
|
||||
"app": {
|
||||
"exec": [
|
||||
"/usr/bin/reduce-worker"
|
||||
],
|
||||
"user": "100",
|
||||
"group": "300",
|
||||
"eventHandlers": [
|
||||
{
|
||||
"exec": [
|
||||
"/usr/bin/data-downloader"
|
||||
],
|
||||
"name": "pre-start"
|
||||
},
|
||||
{
|
||||
"exec": [
|
||||
"/usr/bin/deregister-worker"
|
||||
],
|
||||
"name": "post-stop"
|
||||
}
|
||||
],
|
||||
"environment": {
|
||||
"REDUCE_WORKER_DEBUG": "true"
|
||||
},
|
||||
"isolators": [
|
||||
{
|
||||
"name": "private-network",
|
||||
"val": "true"
|
||||
},
|
||||
{
|
||||
"name": "cpu/shares",
|
||||
"val": "20"
|
||||
},
|
||||
{
|
||||
"name": "memory/limit",
|
||||
"val": "1G"
|
||||
},
|
||||
{
|
||||
"name": "capabilities/bounding-set",
|
||||
"val": "CAP_NET_BIND_SERVICECAP_SYS_ADMIN"
|
||||
}
|
||||
],
|
||||
"mountPoints": [
|
||||
{
|
||||
"name": "database",
|
||||
"path": "/var/lib/db",
|
||||
"readOnly": false
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
{
|
||||
"name": "health",
|
||||
"port": 4000,
|
||||
"protocol": "tcp",
|
||||
"socketActivated": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"dependencies": [
|
||||
{
|
||||
"hash": "sha256-...",
|
||||
"labels": [
|
||||
{
|
||||
"name": "os",
|
||||
"val": "linux"
|
||||
},
|
||||
{
|
||||
"name": "env",
|
||||
"val": "canary"
|
||||
}
|
||||
],
|
||||
"name": "example.com/reduce-worker-base",
|
||||
"root": "/"
|
||||
}
|
||||
],
|
||||
"pathWhitelist": [
|
||||
"/etc/ca/example.com/crt",
|
||||
"/usr/bin/map-reduce-worker",
|
||||
"/opt/libs/reduce-toolkit.so",
|
||||
"/etc/reduce-worker.conf",
|
||||
"/etc/systemd/system/"
|
||||
],
|
||||
"annotations": {
|
||||
"authors": "Carly Container <carly@example.com>, Nat Network <[nat@example.com](mailto:nat@example.com)>",
|
||||
"created": "2014-10-27T19:32:27.67021798Z",
|
||||
"documentation": "https://example.com/docs",
|
||||
"homepage": "https://example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* **acKind** is required and must be set to "ImageManifest"
|
||||
* **acVersion** is required and represents the version of the schema specification that the manifest implements (string, must be in [semver](http://semver.org/) format)
|
||||
* **name** is required, and will be used as a human readable index to the container image. (string, restricted to the AC Name formatting)
|
||||
* **labels** are optional, and should be a list of label objects (where the *name* is restricted to the AC Name formatting and *val* is an arbitrary string). Labels are used during image discovery and dependency resolution. Several well-known labels are defined:
|
||||
* **version** when combined with "name", this should be unique for every build of an app (on a given "os"/"arch" combination).
|
||||
* **os** (currently, the only supported value is "linux"). Together with "arch", this can be considered to describe the syscall ABI this image requires.
|
||||
* **arch** (currently, the only supported value is "amd64"). Together with "os", this can be considered to describe the syscall ABI this image requires.
|
||||
* **app** is optional. If present, this defines the default parameters that can be used to execute this image as an application.
|
||||
* **exec** the executable to launch and any flags (array of strings, must be non-empty; ACE can append or override)
|
||||
* **user**, **group** are required, and indicate either the UID/GID or the username/group name the app should run as inside the container (freeform string). If the user or group field begins with a "/", the owner and group of the file found at that absolute path inside the rootfs is used as the UID/GID of the process.
|
||||
* **eventHandlers** are optional, and should be a list of eventHandler objects. eventHandlers allow the app to have several hooks based on lifecycle events. For example, you may want to execute a script before the main process starts up to download a dataset or backup onto the filesystem. An eventHandler is a simple object with two fields - an **exec** (array of strings, ACE can append or override), and a **name**, which should be one of:
|
||||
* **pre-start** - will be executed and must exit before the long running main **exec** binary is launched
|
||||
* **post-stop** - if the main **exec** process is killed then this is ran. This can be used to cleanup resources in the case of clean application shutdown, but cannot be relied upon in the face of machine failure.stopped
|
||||
* **environment** the app's preferred environment variables (map of freeform strings) (ACE can append)
|
||||
* **mountPoints** are the locations where a container is expecting external data to mounted. The name indicates an executor-defined label to look up a mount point, and the path stipulates where it should actually be mounted inside the rootfs. The name is restricted to the AC Name Type formatting. "readOnly" should be a boolean indicating whether or not the mount point should be read-only (defaults to "false" if unsupplied).
|
||||
* **ports** are the protocols and port numbers that the container will be listening on once started. The key is restricted to the AC Name formatting. This information is primarily informational to help the user find ports that are not well known. It could also optionally be used to limit the inbound connections to the container via firewall rules to only ports that are explicitly exposed.
|
||||
* **socketActivated** if this is set to true then the application expects to be [socket activated](http://www.freedesktop.org/software/systemd/man/sd_listen_fds.html) on these ports. The ACE must pass file descriptors using the [socket activation protocol](http://www.freedesktop.org/software/systemd/man/sd_listen_fds.html) that are listening on these ports when starting this container. If multiple apps in the same container are using socket activation then the ACE must match the sockets to the correct apps using getsockopt() and getsockname().
|
||||
* **isolators** is a list of well-known and optional isolation steps that should be applied to the app. **name** is restricted to the [AC Name](#ac-name-type) formatting and **val** can be a freeform string. Any isolators specified in the App Manifest can be overridden at runtime via the Container Runtime Manifest. The executor can either ignore isolator keys it does not understand or error. In practice this means there might be certain isolators (for example, an AppArmor policy) that an executor doesn't understand so it will simply skip that entry.
|
||||
* **dependencies** list of dependent application images that need to be placed down into the rootfs before the files from this image (if any). The ordering is significant. See [Dependency Matching](#dependency-matching) for how dependencies should be retrieved.
|
||||
* **name** name of the dependent app image (required).
|
||||
* **hash** content hash of the dependency (optional). If provided, the retrieved dependency must match the hash. This can be used to produce deterministic, repeatable builds of an AppImage that has dependencies.
|
||||
* **labels* are optional, and should be a list of label objects of the same form as in the top level ImageManifest. See [Dependency Matching](#dependency-matching) for how these are used.
|
||||
* **pathWhitelist** (optional, list of strings). This is the complete whitelist of paths that should exist in the rootfs after assembly (i.e. unpacking the files in this image and overlaying its dependencies, in order). Paths that end in slash will ensure the directory is present but empty. This field is only required if the app has dependencies and you wish to remove files from the rootfs before running the container; an empty value means that all files in this image and any dependencies will be available in the rootfs.
|
||||
* **annotations** key/value store that can be used by systems outside of the ACE (ACE can override). The key is restricted to the [AC Name](#ac-name-type) formatting. If you are defining new annotations, please consider submitting them to the specification. If you intend for your field to remain special to your application please be a good citizen and prefix an appropriate namespace to your key names. Recognized annotations include:
|
||||
* **created** is the date on which this container was built (string, must be in [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) format)
|
||||
* **authors** contact details of the people or organization responsible for the containers (freeform string)
|
||||
* **homepage** URL to find more information on the container (string, must be a URL with scheme HTTP or HTTPS)
|
||||
* **documentation** URL to get documentation on this container (string, must be a URL with scheme HTTP or HTTPS)
|
||||
|
||||
#### Dependency Matching
|
||||
|
||||
Dependency matching is based on a combination of the three different fields of the dependency - **name**, **hash**, and **labels**.
|
||||
First, the image discovery mechanism is used to locate a dependency.
|
||||
If any labels are specified in the dependency, they are passed to the image discovery mechanism, and should be used when locating the image.
|
||||
|
||||
If the image discovery process successfully returns an image, it will be compared as follows
|
||||
If the dependency specification has a hash, it will be compared against the image returned, and must match.
|
||||
Otherwise, the labels in the dependency specification are compared against the labels in the retrieved app image (i.e. in its ImageManifest), and must match.
|
||||
A label is considered to match if it meets one of three criteria:
|
||||
- It is present in the dependency specification and present in the dependency's ImageManifest with the same value.
|
||||
- It is absent from the dependency specification and present in the dependency's ImageManifest, with any value.
|
||||
This facilitates "wildcard" matching and a variety of common usage patterns, like "noarch" or "latest" dependencies.
|
||||
For example, an AppImage containing a set of bash scripts might omit both "os" and "arch", and hence could be used as a dependency by a variety of different AppImages.
|
||||
Alternatively, an AppImage might specify a dependency with no hash and no "version" label, and the image discovery mechanism could always retrieve the latest version of an AppImage
|
||||
|
||||
### Container Runtime Manifest Schema
|
||||
|
||||
JSON Schema for the Container Runtime Manifest
|
||||
|
||||
```
|
||||
{
|
||||
|
||||
"acVersion": "0.1.0",
|
||||
"acKind": "ContainerRuntimeManifest",
|
||||
"uuid": "6733C088-A507-4694-AABF-EDBE4FC5266F",
|
||||
"apps": [
|
||||
{
|
||||
"app": "example.com/reduce-worker",
|
||||
"imageID": "sha256-277205b3ae3eb3a8e042a62ae46934b470e431ac"
|
||||
},
|
||||
{
|
||||
"app": "example.com/worker-backup",
|
||||
"imageID": "sha256-3e86b59982e49066c5d813af1c2e2579cbf573de",
|
||||
"isolators": [
|
||||
{"name": "memory/limit" "val": "1G"}
|
||||
],
|
||||
"annotations": {
|
||||
"foo": "baz"
|
||||
}
|
||||
},
|
||||
{
|
||||
"app": "example.com/reduce-worker-register",
|
||||
"imageID": "sha256-86298e1fdb95ec9a45b5935504e26ec29b8feffa"
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"kind": "host",
|
||||
"source": "/opt/tenant1/work",
|
||||
"readOnly": true,
|
||||
"fulfills": [
|
||||
"work"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "empty",
|
||||
"fulfills": [
|
||||
"buildOutput"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"isolators": {
|
||||
{
|
||||
"name": "memory/limit",
|
||||
"value": "4G"
|
||||
}
|
||||
},
|
||||
|
||||
"annotations": {
|
||||
"ip-address": "10.1.2.3"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* **acVersion** is required and represents the version of the schema spec (string, must be in [semver](http://semver.org/) format)
|
||||
* **acKind** is required and must be set to "ContainerRuntimeManifest"
|
||||
* **uuid** an [RFC4122 UUID](http://www.ietf.org/rfc/rfc4122.txt) that represents this instance of the container (string, must be in [RFC4122](http://www.ietf.org/rfc/rfc4122.txt) format)
|
||||
* **apps** the list of apps that will execute inside of this container
|
||||
* **app** the name of the app (string, restricted to AC Name formatting)
|
||||
* **imageID** the content hash of the image that this app will execute inside of (string, must be of the format "type-value", where "type" is "sha256" and value is the hex encoded string of the hash)
|
||||
* **isolators** the list of isolators that should be applied to this app (key is restricted to the AC Name formatting and the value can be a freeform string)
|
||||
* **annotations** arbitrary metadata appended to the app (key is restricted to the AC Name formatting and the value can be a freeform string)
|
||||
* **volumes** the list of volumes which should be mounted into each application's filesystem
|
||||
* **kind** string, currently either "empty" or "host" (bind mount)
|
||||
* **fulfills** the MountPoints of the containers that this volume can fulfill (string, restricted to AC Name formatting)
|
||||
* **isolators** the list of isolators that will apply to all apps in this container (name is restricted to the AC Name formatting and the value can be a freeform string)
|
||||
* **annotations** arbitrary metadata the executor should make available to applications via the metadata service (key is restricted to the AC Name formatting and the value can be a freeform string)
|
||||
The App Container Specification has moved to https://github.com/appc/spec/blob/master/SPEC.md
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
{
|
||||
"acVersion": "1.0.0",
|
||||
"acKind": "ImageManifest",
|
||||
"name": "coreos.com/ace-validator-main",
|
||||
"labels": [
|
||||
{ "name": "version", "val": "1.0.0" },
|
||||
{ "name": "os", "val": "linux" },
|
||||
{ "name": "arch", "val": "amd64" }
|
||||
],
|
||||
"app": {
|
||||
"exec": [
|
||||
"/ace-validator", "main"
|
||||
],
|
||||
"eventHandlers": [
|
||||
{
|
||||
"name": "pre-start",
|
||||
"exec": [
|
||||
"/ace-validator", "prestart"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "post-stop",
|
||||
"exec": [
|
||||
"/ace-validator", "poststop"
|
||||
]
|
||||
}
|
||||
],
|
||||
"user": "0",
|
||||
"group": "0",
|
||||
"environment": {
|
||||
"IN_ACE_VALIDATOR": "correct"
|
||||
},
|
||||
"mountPoints": [
|
||||
{
|
||||
"name": "database",
|
||||
"path": "/db",
|
||||
"readOnly": false
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
{
|
||||
"name": "www",
|
||||
"protocol": "tcp",
|
||||
"port": 80
|
||||
}
|
||||
],
|
||||
"isolators": [
|
||||
{
|
||||
"name": "private-network",
|
||||
"val": "true"
|
||||
},
|
||||
{
|
||||
"name": "memory/limit",
|
||||
"val": "1G"
|
||||
}
|
||||
]
|
||||
},
|
||||
"annotations": {
|
||||
"created": "2014-10-27T19:32:27.67021798Z",
|
||||
"authors": "Carly Container <carly@example.com>, Nat Network <nat@example.com>",
|
||||
"homepage": "https://github.com/containers/standard",
|
||||
"documentation": "https://github.com/containers/standard/blob/master/README.md",
|
||||
"lorem": "ipsum"
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"acVersion": "1.0.0",
|
||||
"acKind": "ImageManifest",
|
||||
"name": "coreos.com/ace-validator-sidekick",
|
||||
"labels": [
|
||||
{ "name": "version", "val": "1.0.0" },
|
||||
{ "name": "os", "val": "linux" },
|
||||
{ "name": "arch", "val": "amd64" }
|
||||
],
|
||||
"app": {
|
||||
"exec": [
|
||||
"/ace-validator", "sidekick"
|
||||
],
|
||||
"user": "0",
|
||||
"group": "0",
|
||||
"mountPoints": [
|
||||
{
|
||||
"name": "database",
|
||||
"path": "/db",
|
||||
"readOnly": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
#!/bin/bash -eu
|
||||
#
|
||||
# Builds an ACI containing a go implementation of an ACE validator
|
||||
#
|
||||
|
||||
PREFIX="app-container/ace"
|
||||
|
||||
if ! [[ $0 =~ "${PREFIX}/build_aci" ]]; then
|
||||
echo "invoke from repository root" 1>&2
|
||||
exit 255
|
||||
fi
|
||||
|
||||
for typ in main sidekick; do
|
||||
layoutdir="bin/ace_${typ}_layout"
|
||||
mkdir -p ${layoutdir}/rootfs
|
||||
cp bin/ace-validator ${layoutdir}/rootfs/
|
||||
cp ${PREFIX}/app_manifest_${typ}.json ${layoutdir}/app
|
||||
# now build the tarball, and sign it
|
||||
pushd ${layoutdir} >/dev/null
|
||||
# Set a consistent timestamp so we get a consistent hash
|
||||
# TODO(jonboulle): make this cleaner..
|
||||
for path in rootfs rootfs/ace-validator; do
|
||||
touch -a -m -d 1415660606 ${path}
|
||||
done
|
||||
../actool build --overwrite --app-manifest app rootfs/ ../ace-validator-${typ}.aci
|
||||
# TODO(jonboulle): create uncompressed instead, then gzip?
|
||||
HASH=sha256-$(gzip -d -f ../ace-validator-${typ}.aci -c | sha256sum - | awk '{print $1}')
|
||||
gpg --cipher-algo AES256 --output ace-validator-${typ}.sig --detach-sig ../ace-validator-${typ}.aci
|
||||
mv ace-validator-${typ}.sig ../
|
||||
popd >/dev/null
|
||||
echo "Wrote ${typ} layout to ${layoutdir}"
|
||||
echo "Wrote unsigned ${typ} ACI bin/ace-validator-${typ}.aci"
|
||||
ln -s ${PWD}/bin/ace-validator-${typ}.aci bin/${HASH}
|
||||
echo "Wrote ${typ} layout hash bin/${HASH}"
|
||||
echo "Wrote ${typ} ACI signature bin/ace-validator-${typ}.sig"
|
||||
done
|
||||
@@ -1,533 +0,0 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
|
||||
This validator tool is intended to be run within an App Container Executor
|
||||
(ACE), and verifies that the ACE has been set up correctly.
|
||||
|
||||
This verifies the _apps perspective_ of the execution environment.
|
||||
|
||||
Changes to the validator need to be reflected in app_manifest.json, and vice-versa
|
||||
|
||||
The App Container Execution spec defines the following expectations within the execution environment:
|
||||
- Working directory always the root of the container
|
||||
- PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
- USER, LOGNAME username of the user executing this app
|
||||
- HOME home directory of the user
|
||||
- SHELL login shell of the user
|
||||
- AC_APP_NAME the entrypoint that this process was defined from
|
||||
|
||||
In addition, we validate:
|
||||
- The expected mount points are mounted
|
||||
- metadata service reachable at http://169.254.169.255
|
||||
|
||||
TODO(jonboulle):
|
||||
- metadata service reachable at AC_METADATA_URL
|
||||
|
||||
TODO(jonboulle):
|
||||
- should we validate Isolators? (e.g. MemoryLimit + malloc, or capabilities)
|
||||
- should we validate ports? (e.g. that they are available to bind to within the network namespace of the container)
|
||||
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/rocket/app-container/schema"
|
||||
"github.com/coreos/rocket/app-container/schema/types"
|
||||
)
|
||||
|
||||
const (
|
||||
standardPath = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
appNameEnv = "AC_APP_NAME"
|
||||
metadataURLBase = "http://169.254.169.255/acMetadata/v1"
|
||||
|
||||
// marker files to validate
|
||||
prestartFile = "/prestart"
|
||||
mainFile = "/main"
|
||||
poststopFile = "/poststop"
|
||||
|
||||
mainVolFile = "/db/main"
|
||||
sidekickVolFile = "/db/sidekick"
|
||||
|
||||
timeout = 5 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
// Expected values must be kept in sync with app_manifest.json
|
||||
// "Environment"
|
||||
env = map[string]string{
|
||||
"IN_ACE_VALIDATOR": "correct",
|
||||
"HOME": "/root",
|
||||
"USER": "root",
|
||||
"LOGNAME": "root",
|
||||
"SHELL": "/bin/sh",
|
||||
}
|
||||
// "MountPoints"
|
||||
mps = map[string]types.MountPoint{
|
||||
"database": types.MountPoint{
|
||||
Path: "/db",
|
||||
ReadOnly: false,
|
||||
},
|
||||
}
|
||||
// "Name"
|
||||
an = "coreos.com/ace-validator-main"
|
||||
)
|
||||
|
||||
type results []error
|
||||
|
||||
// main outputs diagnostic information to stderr and exits 1 if validation fails
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Fprintf(os.Stderr, "usage: %s [main|sidekick|preStart|postStop]\n", os.Args[0])
|
||||
os.Exit(64)
|
||||
}
|
||||
mode := os.Args[1]
|
||||
var res results
|
||||
switch strings.ToLower(mode) {
|
||||
case "main":
|
||||
res = validateMain()
|
||||
case "sidekick":
|
||||
res = validateSidekick()
|
||||
case "prestart":
|
||||
res = validatePrestart()
|
||||
case "poststop":
|
||||
res = validatePoststop()
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unrecognized mode: %s\b", mode)
|
||||
os.Exit(64)
|
||||
}
|
||||
if len(res) == 0 {
|
||||
fmt.Printf("%s OK\n", mode)
|
||||
os.Exit(0)
|
||||
}
|
||||
fmt.Printf("%s FAIL\n", mode)
|
||||
for _, err := range res {
|
||||
fmt.Fprintln(os.Stderr, "==>", err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func validateMain() (errs results) {
|
||||
errs = append(errs, assertExists(prestartFile)...)
|
||||
errs = append(errs, assertNotExistsAndCreate(mainFile)...)
|
||||
errs = append(errs, assertNotExists(poststopFile)...)
|
||||
errs = append(errs, ValidatePath(standardPath)...)
|
||||
errs = append(errs, ValidateEnvironment(env)...)
|
||||
errs = append(errs, ValidateMountpoints(mps)...)
|
||||
errs = append(errs, ValidateAppNameEnv(an)...)
|
||||
errs = append(errs, ValidateMetadataSvc()...)
|
||||
errs = append(errs, waitForFile(sidekickVolFile, timeout)...)
|
||||
errs = append(errs, assertNotExistsAndCreate(mainVolFile)...)
|
||||
return
|
||||
}
|
||||
|
||||
func validateSidekick() (errs results) {
|
||||
errs = append(errs, assertNotExistsAndCreate(sidekickVolFile)...)
|
||||
errs = append(errs, waitForFile(mainVolFile, timeout)...)
|
||||
return
|
||||
}
|
||||
|
||||
func validatePrestart() (errs results) {
|
||||
errs = append(errs, assertNotExistsAndCreate(prestartFile)...)
|
||||
errs = append(errs, assertNotExists(mainFile)...)
|
||||
errs = append(errs, assertNotExists(poststopFile)...)
|
||||
return
|
||||
}
|
||||
|
||||
func validatePoststop() (errs results) {
|
||||
errs = append(errs, assertExists(prestartFile)...)
|
||||
errs = append(errs, assertExists(mainFile)...)
|
||||
errs = append(errs, assertNotExistsAndCreate(poststopFile)...)
|
||||
return
|
||||
}
|
||||
|
||||
// ValidatePath ensures that the PATH has been set up correctly within the
|
||||
// environment in which this process is being run
|
||||
func ValidatePath(wp string) results {
|
||||
r := results{}
|
||||
gp := os.Getenv("PATH")
|
||||
if wp != gp {
|
||||
r = append(r, fmt.Errorf("PATH not set appropriately (need %q)", wp))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// ValidateEnvironment ensures that the given environment exactly maps the
|
||||
// environment in which this process is running
|
||||
func ValidateEnvironment(wenv map[string]string) (r results) {
|
||||
for wkey, wval := range wenv {
|
||||
gval := os.Getenv(wkey)
|
||||
if gval != wval {
|
||||
err := fmt.Errorf("environment variable %q not set as expected (need %q)", wkey, wval)
|
||||
r = append(r, err)
|
||||
}
|
||||
}
|
||||
for _, s := range os.Environ() {
|
||||
parts := strings.SplitN(s, "=", 2)
|
||||
k := parts[0]
|
||||
_, ok := wenv[k]
|
||||
switch {
|
||||
case k == appNameEnv, k == "PATH", k == "TERM":
|
||||
case !ok:
|
||||
r = append(r, fmt.Errorf("unexpected environment variable %q set", k))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ValidateAppNameEnv ensures that the environment variable specifying the
|
||||
// entrypoint of this process is set correctly.
|
||||
func ValidateAppNameEnv(want string) (r results) {
|
||||
if got := os.Getenv(appNameEnv); got != want {
|
||||
r = append(r, fmt.Errorf("%s not set appropriately", appNameEnv))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ValidateMountpoints ensures that the given mount points are present in the
|
||||
// environment in which this process is running
|
||||
func ValidateMountpoints(wmp map[string]types.MountPoint) results {
|
||||
r := results{}
|
||||
// TODO(jonboulle): verify actual source
|
||||
for _, mp := range wmp {
|
||||
if err := checkMount(mp.Path, mp.ReadOnly); err != nil {
|
||||
r = append(r, err)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func metadataRequest(req *http.Request) ([]byte, error) {
|
||||
cli := http.Client{
|
||||
Timeout: 100 * time.Millisecond,
|
||||
}
|
||||
|
||||
req.Header["Metadata-Flavor"] = []string{"AppContainer header"}
|
||||
|
||||
resp, err := cli.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("Get %s failed with %v", req.URL, resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Get %s failed on body read: %v", req.URL, err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func metadataGet(path string) ([]byte, error) {
|
||||
req, err := http.NewRequest("GET", metadataURLBase+path, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return metadataRequest(req)
|
||||
}
|
||||
|
||||
func metadataPost(path string, body []byte) ([]byte, error) {
|
||||
req, err := http.NewRequest("POST", metadataURLBase+path, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "text/plan")
|
||||
|
||||
return metadataRequest(req)
|
||||
}
|
||||
|
||||
func metadataPostForm(path string, data url.Values) ([]byte, error) {
|
||||
req, err := http.NewRequest("POST", metadataURLBase+path, strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
return metadataRequest(req)
|
||||
}
|
||||
|
||||
func validateContainerAnnotations(crm *schema.ContainerRuntimeManifest) results {
|
||||
r := results{}
|
||||
|
||||
actualAnnots := make(map[types.ACName]string)
|
||||
|
||||
annots, err := metadataGet("/container/annotations/")
|
||||
if err != nil {
|
||||
return append(r, err)
|
||||
}
|
||||
|
||||
for _, key := range strings.Split(string(annots), "\n") {
|
||||
val, err := metadataGet("/container/annotations/" + key)
|
||||
if err != nil {
|
||||
r = append(r, err)
|
||||
}
|
||||
|
||||
lbl, err := types.NewACName(key)
|
||||
if err != nil {
|
||||
r = append(r, fmt.Errorf("invalid annotation label: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
actualAnnots[*lbl] = string(val)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actualAnnots, crm.Annotations) {
|
||||
r = append(r, fmt.Errorf("container annotations mismatch: %v vs %v", actualAnnots, crm.Annotations))
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func validateContainerMetadata(crm *schema.ContainerRuntimeManifest) results {
|
||||
r := results{}
|
||||
|
||||
uid, err := metadataGet("/container/uid")
|
||||
if err != nil {
|
||||
return append(r, err)
|
||||
}
|
||||
if strings.ToLower(string(uid)) != strings.ToLower(crm.UUID.String()) {
|
||||
return append(r, fmt.Errorf("UUID mismatch: %v vs %v", string(uid), crm.UUID.String()))
|
||||
}
|
||||
|
||||
return append(r, validateContainerAnnotations(crm)...)
|
||||
}
|
||||
|
||||
func validateAppAnnotations(crm *schema.ContainerRuntimeManifest, app *schema.ImageManifest) results {
|
||||
r := results{}
|
||||
|
||||
// build a map of expected annotations by merging app.Annotations
|
||||
// with ContainerRuntimeManifest overrides
|
||||
expectedAnnots := app.Annotations
|
||||
if expectedAnnots == nil {
|
||||
expectedAnnots = make(types.Annotations)
|
||||
}
|
||||
a := crm.Apps.Get(app.Name)
|
||||
if a == nil {
|
||||
panic("could not find app in manifest!")
|
||||
}
|
||||
for k, v := range a.Annotations {
|
||||
expectedAnnots[k] = v
|
||||
}
|
||||
|
||||
actualAnnots := make(types.Annotations)
|
||||
|
||||
annots, err := metadataGet("/apps/" + string(app.Name) + "/annotations/")
|
||||
if err != nil {
|
||||
return append(r, err)
|
||||
}
|
||||
|
||||
for _, key := range strings.Split(string(annots), "\n") {
|
||||
if len(key) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
val, err := metadataGet("/apps/" + string(app.Name) + "/annotations/" + key)
|
||||
if err != nil {
|
||||
r = append(r, err)
|
||||
}
|
||||
|
||||
lbl, err := types.NewACName(key)
|
||||
if err != nil {
|
||||
r = append(r, fmt.Errorf("invalid annotation label: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
actualAnnots[*lbl] = string(val)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actualAnnots, expectedAnnots) {
|
||||
err := fmt.Errorf("%v annotations mismatch: %v vs %v", app.Name, actualAnnots, expectedAnnots)
|
||||
r = append(r, err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func validateAppMetadata(crm *schema.ContainerRuntimeManifest, a schema.RuntimeApp) results {
|
||||
appName := a.Name
|
||||
r := results{}
|
||||
|
||||
am, err := metadataGet("/apps/" + string(appName) + "/image/manifest")
|
||||
if err != nil {
|
||||
return append(r, err)
|
||||
}
|
||||
|
||||
app := &schema.ImageManifest{}
|
||||
if err = json.Unmarshal(am, app); err != nil {
|
||||
return append(r, fmt.Errorf("failed to JSON-decode %q manifest: %v", string(appName), err))
|
||||
}
|
||||
|
||||
id, err := metadataGet("/apps/" + string(appName) + "/image/id")
|
||||
if err != nil {
|
||||
r = append(r, err)
|
||||
}
|
||||
|
||||
if string(id) != a.ImageID.String() {
|
||||
err = fmt.Errorf("%q's image id mismatch: %v vs %v", string(appName), id, a.ImageID)
|
||||
r = append(r, err)
|
||||
}
|
||||
|
||||
return append(r, validateAppAnnotations(crm, app)...)
|
||||
}
|
||||
|
||||
func validateSigning(crm *schema.ContainerRuntimeManifest) results {
|
||||
r := results{}
|
||||
|
||||
plaintext := "Old MacDonald Had A Farm"
|
||||
|
||||
// Sign
|
||||
sig, err := metadataPost("/container/hmac/sign", []byte(plaintext))
|
||||
if err != nil {
|
||||
return append(r, err)
|
||||
}
|
||||
|
||||
// Verify
|
||||
_, err = metadataPostForm("/container/hmac/verify", url.Values{
|
||||
"uid": []string{crm.UUID.String()},
|
||||
"signature": []string{string(sig)},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return append(r, err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func ValidateMetadataSvc() results {
|
||||
r := results{}
|
||||
|
||||
cm, err := metadataGet("/container/manifest")
|
||||
if err != nil {
|
||||
return append(r, err)
|
||||
}
|
||||
|
||||
crm := &schema.ContainerRuntimeManifest{}
|
||||
if err = json.Unmarshal(cm, crm); err != nil {
|
||||
return append(r, fmt.Errorf("failed to JSON-decode container manifest: %v", err))
|
||||
}
|
||||
|
||||
r = append(r, validateContainerMetadata(crm)...)
|
||||
|
||||
for _, app := range crm.Apps {
|
||||
app := app
|
||||
r = append(r, validateAppMetadata(crm, app)...)
|
||||
}
|
||||
|
||||
return append(r, validateSigning(crm)...)
|
||||
}
|
||||
|
||||
// checkMount checks that the given string is a mount point, and that it is
|
||||
// mounted appropriately read-only or not according to the given bool
|
||||
func checkMount(d string, readonly bool) error {
|
||||
// or....
|
||||
// os.Stat(path).Sys().(*syscall.Stat_t).Dev
|
||||
sfs1 := &syscall.Statfs_t{}
|
||||
if err := syscall.Statfs(d, sfs1); err != nil {
|
||||
return fmt.Errorf("error calling statfs on %q: %v", d, err)
|
||||
}
|
||||
sfs2 := &syscall.Statfs_t{}
|
||||
if err := syscall.Statfs(filepath.Dir(d), sfs2); err != nil {
|
||||
return fmt.Errorf("error calling statfs on %q: %v", d, err)
|
||||
}
|
||||
if sfs1.Fsid == sfs2.Fsid {
|
||||
return fmt.Errorf("%q is not a mount point", d)
|
||||
}
|
||||
ro := sfs1.Flags&syscall.O_RDONLY == 1
|
||||
if ro != readonly {
|
||||
return fmt.Errorf("%q mounted ro=%t, want %t", d, ro, readonly)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// assertNotExistsAndCreate asserts that a file at the given path does not
|
||||
// exist, and then proceeds to create (touch) the file. It returns any errors
|
||||
// encountered at either of these steps.
|
||||
func assertNotExistsAndCreate(p string) []error {
|
||||
var errs []error
|
||||
errs = append(errs, assertNotExists(p)...)
|
||||
if err := touchFile(p); err != nil {
|
||||
errs = append(errs, fmt.Errorf("error touching file %q: %v", p, err))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// assertNotExists asserts that a file at the given path does not exist. A
|
||||
// non-empty list of errors is returned if the file exists or any error is
|
||||
// encountered while checking.
|
||||
func assertNotExists(p string) []error {
|
||||
var errs []error
|
||||
e, err := fileExists(p)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("error checking %q exists: %v", p, err))
|
||||
}
|
||||
if e {
|
||||
errs = append(errs, fmt.Errorf("file %q exists unexpectedly", p))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// assertExists asserts that a file exists at the given path. A non-empty
|
||||
// list of errors is returned if the file does not exist or any error is
|
||||
// encountered while checking.
|
||||
func assertExists(p string) []error {
|
||||
var errs []error
|
||||
e, err := fileExists(p)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("error checking %q exists: %v", p, err))
|
||||
}
|
||||
if !e {
|
||||
errs = append(errs, fmt.Errorf("file %q does not exist as expected", p))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// touchFile creates an empty file, returning any error encountered
|
||||
func touchFile(p string) error {
|
||||
_, err := os.Create(p)
|
||||
return err
|
||||
}
|
||||
|
||||
// fileExists checks whether a file exists at the given path
|
||||
func fileExists(p string) (bool, error) {
|
||||
_, err := os.Stat(p)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
// waitForFile waits for the file at the given path to appear
|
||||
func waitForFile(p string, to time.Duration) []error {
|
||||
done := time.After(to)
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return []error{
|
||||
fmt.Errorf("timed out waiting for %s", p),
|
||||
}
|
||||
case <-time.After(1):
|
||||
if ok, _ := fileExists(p); ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package aci
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type FileType string
|
||||
|
||||
const (
|
||||
TypeGzip = FileType("gz")
|
||||
TypeBzip2 = FileType("bz2")
|
||||
TypeXz = FileType("xz")
|
||||
TypeTar = FileType("tar")
|
||||
TypeText = FileType("text")
|
||||
TypeUnknown = FileType("unknown")
|
||||
|
||||
readLen = 512 // max bytes to sniff
|
||||
|
||||
hexHdrGzip = "1f8b"
|
||||
hexHdrBzip2 = "425a68"
|
||||
hexHdrXz = "fd377a585a00"
|
||||
hexSigTar = "7573746172"
|
||||
|
||||
tarOffset = 257
|
||||
|
||||
textMime = "text/plain; charset=utf-8"
|
||||
)
|
||||
|
||||
var (
|
||||
hdrGzip []byte
|
||||
hdrBzip2 []byte
|
||||
hdrXz []byte
|
||||
sigTar []byte
|
||||
tarEnd int
|
||||
)
|
||||
|
||||
func mustDecodeHex(s string) []byte {
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func init() {
|
||||
hdrGzip = mustDecodeHex(hexHdrGzip)
|
||||
hdrBzip2 = mustDecodeHex(hexHdrBzip2)
|
||||
hdrXz = mustDecodeHex(hexHdrXz)
|
||||
sigTar = mustDecodeHex(hexSigTar)
|
||||
tarEnd = tarOffset + len(sigTar)
|
||||
}
|
||||
|
||||
// DetectFileType attempts to detect the type of file that the given reader
|
||||
// represents by comparing it against known file signatures (magic numbers)
|
||||
func DetectFileType(r io.Reader) (FileType, error) {
|
||||
var b bytes.Buffer
|
||||
n, err := io.CopyN(&b, r, readLen)
|
||||
if err != nil && err != io.EOF {
|
||||
return TypeUnknown, err
|
||||
}
|
||||
bs := b.Bytes()
|
||||
switch {
|
||||
case bytes.HasPrefix(bs, hdrGzip):
|
||||
return TypeGzip, nil
|
||||
case bytes.HasPrefix(bs, hdrBzip2):
|
||||
return TypeBzip2, nil
|
||||
case bytes.HasPrefix(bs, hdrXz):
|
||||
return TypeXz, nil
|
||||
case n > int64(tarEnd) && bytes.Equal(bs[tarOffset:tarEnd], sigTar):
|
||||
return TypeTar, nil
|
||||
case http.DetectContentType(bs) == textMime:
|
||||
return TypeText, nil
|
||||
default:
|
||||
return TypeUnknown, nil
|
||||
}
|
||||
}
|
||||
|
||||
// XzReader shells out to a command line xz executable (if
|
||||
// available) to decompress the given io.Reader using the xz
|
||||
// compression format
|
||||
func XzReader(r io.Reader) io.ReadCloser {
|
||||
rpipe, wpipe := io.Pipe()
|
||||
ex, err := exec.LookPath("xz")
|
||||
if err != nil {
|
||||
log.Fatalf("couldn't find xz executable: %v", err)
|
||||
}
|
||||
cmd := exec.Command(ex, "--decompress", "--stdout")
|
||||
cmd.Stdin = r
|
||||
cmd.Stdout = wpipe
|
||||
|
||||
go func() {
|
||||
err := cmd.Run()
|
||||
wpipe.CloseWithError(err)
|
||||
}()
|
||||
|
||||
return rpipe
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package aci
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
|
||||
"github.com/coreos/rocket/Godeps/_workspace/src/golang.org/x/crypto/openpgp"
|
||||
)
|
||||
|
||||
// TODO(jonboulle): support detached signatures
|
||||
|
||||
// LoadSignedData reads PGP encrypted data from the given Reader, using the
|
||||
// provided keyring (EntityList). The entire decrypted bytestream is
|
||||
// returned, and/or any error encountered.
|
||||
// TODO(jonboulle): support symmetric decryption
|
||||
func LoadSignedData(signed io.Reader, kr openpgp.EntityList) ([]byte, error) {
|
||||
md, err := openpgp.ReadMessage(signed, kr, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if md.IsSymmetricallyEncrypted {
|
||||
return nil, errors.New("symmetric encryption not yet supported")
|
||||
}
|
||||
|
||||
// Signature cannot be verified until body is read
|
||||
data, err := ioutil.ReadAll(md.UnverifiedBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading body: %v", err)
|
||||
}
|
||||
if md.IsSigned && md.SignedBy != nil {
|
||||
// Once EOF has been seen, the following fields are
|
||||
// valid. (An authentication code failure is reported as a
|
||||
// SignatureError error when reading from UnverifiedBody.)
|
||||
//
|
||||
if md.SignatureError != nil {
|
||||
return nil, fmt.Errorf("signature error: %v", md.SignatureError)
|
||||
}
|
||||
log.Println("message signature OK")
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
package aci
|
||||
|
||||
/*
|
||||
|
||||
Image Layout
|
||||
|
||||
The on-disk layout of an app container is straightforward.
|
||||
It includes a rootfs with all of the files that will exist in the root of the app and a manifest describing the image.
|
||||
The layout must contain an app image manifest.
|
||||
|
||||
/manifest
|
||||
/rootfs/
|
||||
/rootfs/usr/bin/mysql
|
||||
|
||||
*/
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/rocket/app-container/schema"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoRootFS = errors.New("no rootfs found in layout")
|
||||
ErrNoManifest = errors.New("no app image manifest found in layout")
|
||||
)
|
||||
|
||||
// ValidateLayout takes a directory and validates that the layout of the directory
|
||||
// matches that expected by the Application Container Image format.
|
||||
// If any errors are encountered during the validation, it will abort and
|
||||
// return the first one.
|
||||
func ValidateLayout(dir string) error {
|
||||
fi, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error accessing layout: %v", err)
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return fmt.Errorf("given path %q is not a directory", dir)
|
||||
}
|
||||
var flist []string
|
||||
var imOK, rfsOK bool
|
||||
var im io.Reader
|
||||
walkLayout := func(fpath string, fi os.FileInfo, err error) error {
|
||||
rpath, err := filepath.Rel(dir, fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := filepath.Base(rpath)
|
||||
switch name {
|
||||
case ".":
|
||||
case "app":
|
||||
im, err = os.Open(fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imOK = true
|
||||
case "rootfs":
|
||||
if !fi.IsDir() {
|
||||
return errors.New("rootfs is not a directory")
|
||||
}
|
||||
rfsOK = true
|
||||
default:
|
||||
flist = append(flist, rpath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := filepath.Walk(dir, walkLayout); err != nil {
|
||||
return err
|
||||
}
|
||||
return validate(imOK, im, rfsOK, flist)
|
||||
}
|
||||
|
||||
// ValidateLayout takes a *tar.Reader and validates that the layout of the
|
||||
// filesystem the reader encapsulates matches that expected by the
|
||||
// Application Container Image format. If any errors are encountered during
|
||||
// the validation, it will abort and return the first one.
|
||||
func ValidateArchive(tr *tar.Reader) error {
|
||||
var flist []string
|
||||
var imOK, rfsOK bool
|
||||
var im bytes.Buffer
|
||||
Tar:
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
switch {
|
||||
case err == nil:
|
||||
case err == io.EOF:
|
||||
break Tar
|
||||
default:
|
||||
return err
|
||||
}
|
||||
name := filepath.Clean(hdr.Name)
|
||||
switch name {
|
||||
case ".":
|
||||
case "app":
|
||||
_, err := io.Copy(&im, tr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imOK = true
|
||||
case "rootfs":
|
||||
if !hdr.FileInfo().IsDir() {
|
||||
return fmt.Errorf("rootfs is not a directory")
|
||||
}
|
||||
rfsOK = true
|
||||
default:
|
||||
flist = append(flist, name)
|
||||
}
|
||||
}
|
||||
return validate(imOK, &im, rfsOK, flist)
|
||||
}
|
||||
|
||||
func validate(imOK bool, im io.Reader, rfsOK bool, files []string) error {
|
||||
if !imOK {
|
||||
return ErrNoManifest
|
||||
}
|
||||
if !rfsOK {
|
||||
return ErrNoRootFS
|
||||
}
|
||||
b, err := ioutil.ReadAll(im)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading app manifest: %v", err)
|
||||
}
|
||||
var a schema.ImageManifest
|
||||
if err := a.UnmarshalJSON(b); err != nil {
|
||||
return fmt.Errorf("app manifest validation failed: %v", err)
|
||||
}
|
||||
for _, f := range files {
|
||||
if !strings.HasPrefix(f, "rootfs") {
|
||||
return fmt.Errorf("unrecognized file path in layout: %q", f)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateImageManifest ensures that the given io.Reader represents a valid
|
||||
// ImageManifest.
|
||||
func validateImageManifest(r io.Reader) error {
|
||||
b, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading app manifest: %v", err)
|
||||
}
|
||||
var im schema.ImageManifest
|
||||
if err = json.Unmarshal(b, &im); err != nil {
|
||||
return fmt.Errorf("error unmarshaling app manifest: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package aci
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/rocket/app-container/schema"
|
||||
)
|
||||
|
||||
// ArchiveWriter writes App Container Images. Users wanting to create an ACI or
|
||||
// should create an ArchiveWriter and add files to it; the ACI will be written
|
||||
// to the underlying tar.Writer
|
||||
type ArchiveWriter interface {
|
||||
AddFile(path string, hdr *tar.Header, r io.Reader) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type appArchiveWriter struct {
|
||||
*tar.Writer
|
||||
am *schema.ImageManifest
|
||||
}
|
||||
|
||||
// NewAppWriter creates a new ArchiveWriter which will generate an App
|
||||
// Container Image based on the given manifest and write it to the given
|
||||
// tar.Writer
|
||||
func NewAppWriter(am schema.ImageManifest, w *tar.Writer) ArchiveWriter {
|
||||
aw := &appArchiveWriter{
|
||||
w,
|
||||
&am,
|
||||
}
|
||||
return aw
|
||||
}
|
||||
|
||||
func (aw *appArchiveWriter) AddFile(path string, hdr *tar.Header, r io.Reader) error {
|
||||
err := aw.Writer.WriteHeader(hdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r != nil {
|
||||
_, err := io.Copy(aw.Writer, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (aw *appArchiveWriter) addFileNow(path string, contents []byte) error {
|
||||
buf := bytes.NewBuffer(contents)
|
||||
now := time.Now()
|
||||
hdr := tar.Header{
|
||||
Name: path,
|
||||
Mode: 0644,
|
||||
Uid: 0,
|
||||
Gid: 0,
|
||||
Size: int64(buf.Len()),
|
||||
ModTime: now,
|
||||
Typeflag: tar.TypeReg,
|
||||
Uname: "root",
|
||||
Gname: "root",
|
||||
ChangeTime: now,
|
||||
}
|
||||
return aw.AddFile(path, &hdr, buf)
|
||||
}
|
||||
|
||||
func (aw *appArchiveWriter) addManifest(name string, m json.Marshaler) error {
|
||||
out, err := m.MarshalJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return aw.addFileNow(name, out)
|
||||
}
|
||||
|
||||
func (aw *appArchiveWriter) Close() error {
|
||||
if err := aw.addManifest("app", aw.am); err != nil {
|
||||
return err
|
||||
}
|
||||
return aw.Writer.Close()
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
const (
|
||||
cliName = "actool"
|
||||
cliDescription = "actool, the application container tool"
|
||||
)
|
||||
|
||||
var (
|
||||
globalFlagset = flag.NewFlagSet(cliName, flag.ExitOnError)
|
||||
out *tabwriter.Writer
|
||||
commands []*Command
|
||||
globalFlags = struct {
|
||||
Dir string
|
||||
Debug bool
|
||||
Help bool
|
||||
}{}
|
||||
transportFlags = struct {
|
||||
Insecure bool
|
||||
}{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
globalFlagset.BoolVar(&globalFlags.Help, "help", false, "Print usage information and exit")
|
||||
globalFlagset.BoolVar(&globalFlags.Debug, "debug", false, "Print verbose (debug) output")
|
||||
}
|
||||
|
||||
type Command struct {
|
||||
Name string // Name of the Command and the string to use to invoke it
|
||||
Summary string // One-sentence summary of what the Command does
|
||||
Usage string // Usage options/arguments
|
||||
Description string // Detailed description of command
|
||||
Flags flag.FlagSet // Set of flags associated with this command
|
||||
|
||||
Run func(args []string) int // Run a command with the given arguments, return exit status
|
||||
}
|
||||
|
||||
func init() {
|
||||
out = new(tabwriter.Writer)
|
||||
out.Init(os.Stdout, 0, 8, 1, '\t', 0)
|
||||
commands = []*Command{
|
||||
cmdBuild,
|
||||
cmdDiscover,
|
||||
cmdHelp,
|
||||
cmdValidate,
|
||||
cmdVersion,
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// parse global arguments
|
||||
globalFlagset.Parse(os.Args[1:])
|
||||
args := globalFlagset.Args()
|
||||
if len(args) < 1 || globalFlags.Help {
|
||||
args = []string{"help"}
|
||||
}
|
||||
|
||||
var cmd *Command
|
||||
|
||||
// determine which Command should be run
|
||||
for _, c := range commands {
|
||||
if c.Name == args[0] {
|
||||
cmd = c
|
||||
if err := c.Flags.Parse(args[1:]); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if cmd == nil {
|
||||
fmt.Fprintf(os.Stderr, "%v: unknown subcommand: %q\n", cliName, args[0])
|
||||
fmt.Fprintf(os.Stderr, "Run '%v help' for usage.\n", cliName)
|
||||
os.Exit(2)
|
||||
}
|
||||
os.Exit(cmd.Run(cmd.Flags.Args()))
|
||||
}
|
||||
|
||||
func getAllFlags() (flags []*flag.Flag) {
|
||||
return getFlags(globalFlagset)
|
||||
}
|
||||
|
||||
func getFlags(flagset *flag.FlagSet) (flags []*flag.Flag) {
|
||||
flags = make([]*flag.Flag, 0)
|
||||
flagset.VisitAll(func(f *flag.Flag) {
|
||||
flags = append(flags, f)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func stderr(format string, a ...interface{}) {
|
||||
out := fmt.Sprintf(format, a...)
|
||||
fmt.Fprintln(os.Stderr, strings.TrimSuffix(out, "\n"))
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/coreos/rocket/app-container/aci"
|
||||
"github.com/coreos/rocket/app-container/schema"
|
||||
"github.com/coreos/rocket/pkg/tarheader"
|
||||
)
|
||||
|
||||
var (
|
||||
buildImageManifest string
|
||||
buildRootfs bool
|
||||
buildOverwrite bool
|
||||
cmdBuild = &Command{
|
||||
Name: "build",
|
||||
Description: "Build an ACI from the target directory",
|
||||
Summary: "Build an ACI from the target directory",
|
||||
Usage: "[--overwrite] --name=NAME DIRECTORY OUTPUT_FILE",
|
||||
Run: runBuild,
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmdBuild.Flags.StringVar(&buildImageManifest, "app-manifest", "",
|
||||
"Build an App Image with this App Manifest")
|
||||
cmdBuild.Flags.BoolVar(&buildRootfs, "rootfs", true,
|
||||
"Whether the supplied directory is a rootfs. If false, it will be assume the supplied directory already contains a rootfs/ subdirectory.")
|
||||
cmdBuild.Flags.BoolVar(&buildOverwrite, "overwrite", false, "Overwrite target file if it already exists")
|
||||
}
|
||||
|
||||
func buildWalker(root string, aw aci.ArchiveWriter, rootfs bool) filepath.WalkFunc {
|
||||
// cache of inode -> filepath, used to leverage hard links in the archive
|
||||
inos := map[uint64]string{}
|
||||
return func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relpath, err := filepath.Rel(root, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rootfs {
|
||||
if relpath == "." {
|
||||
relpath = ""
|
||||
}
|
||||
relpath = "rootfs/" + relpath
|
||||
}
|
||||
if relpath == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
link := ""
|
||||
var file *os.File
|
||||
switch info.Mode() & os.ModeType {
|
||||
default:
|
||||
file, err = os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
case os.ModeSymlink:
|
||||
target, err := os.Readlink(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
link = target
|
||||
}
|
||||
|
||||
hdr, err := tar.FileInfoHeader(info, link)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Because os.FileInfo's Name method returns only the base
|
||||
// name of the file it describes, it may be necessary to
|
||||
// modify the Name field of the returned header to provide the
|
||||
// full path name of the file.
|
||||
hdr.Name = relpath
|
||||
tarheader.Populate(hdr, info, inos)
|
||||
// If the file is a hard link we don't need the contents
|
||||
if hdr.Typeflag == tar.TypeLink {
|
||||
hdr.Size = 0
|
||||
file = nil
|
||||
}
|
||||
aw.AddFile(relpath, hdr, file)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func runBuild(args []string) (exit int) {
|
||||
if len(args) != 2 {
|
||||
stderr("build: Must provide directory and output file")
|
||||
return 1
|
||||
}
|
||||
if buildImageManifest == "" {
|
||||
stderr("build: must specify --app-manifest")
|
||||
return 1
|
||||
}
|
||||
|
||||
root := args[0]
|
||||
tgt := args[1]
|
||||
ext := filepath.Ext(tgt)
|
||||
if ext != schema.ACIExtension {
|
||||
stderr("build: Extension must be %s (given %s)", schema.ACIExtension, ext)
|
||||
return 1
|
||||
}
|
||||
|
||||
mode := os.O_CREATE | os.O_WRONLY
|
||||
if !buildOverwrite {
|
||||
mode |= os.O_EXCL
|
||||
}
|
||||
fh, err := os.OpenFile(tgt, mode, 0644)
|
||||
if err != nil {
|
||||
if os.IsExist(err) {
|
||||
stderr("build: Target file exists (try --overwrite)")
|
||||
} else {
|
||||
stderr("build: Unable to open target %s: %v", tgt, err)
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
gw := gzip.NewWriter(fh)
|
||||
tr := tar.NewWriter(gw)
|
||||
|
||||
defer func() {
|
||||
tr.Close()
|
||||
gw.Close()
|
||||
fh.Close()
|
||||
if exit != 0 && !buildOverwrite {
|
||||
os.Remove(tgt)
|
||||
}
|
||||
}()
|
||||
|
||||
b, err := ioutil.ReadFile(buildImageManifest)
|
||||
if err != nil {
|
||||
stderr("build: Unable to read App Manifest: %v", err)
|
||||
return 1
|
||||
}
|
||||
var am schema.ImageManifest
|
||||
if err := am.UnmarshalJSON(b); err != nil {
|
||||
stderr("build: Unable to load App Manifest: %v", err)
|
||||
return 1
|
||||
}
|
||||
aw := aci.NewAppWriter(am, tr)
|
||||
|
||||
err = filepath.Walk(root, buildWalker(root, aw, buildRootfs))
|
||||
if err != nil {
|
||||
stderr("build: Error walking rootfs: %v", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
err = aw.Close()
|
||||
if err != nil {
|
||||
stderr("build: Unable to close Fileset image %s: %v", tgt, err)
|
||||
return 1
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/rocket/app-container/discovery"
|
||||
)
|
||||
|
||||
var (
|
||||
cmdDiscover = &Command{
|
||||
Name: "discover",
|
||||
Description: "Discover the download URLs for an app",
|
||||
Summary: "Discover the download URLs for one or more app container images",
|
||||
Usage: "APP...",
|
||||
Run: runDiscover,
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmdDiscover.Flags.BoolVar(&transportFlags.Insecure, "insecure", false,
|
||||
"Allow insecure non-TLS downloads over http")
|
||||
}
|
||||
|
||||
func runDiscover(args []string) (exit int) {
|
||||
if len(args) < 1 {
|
||||
fmt.Fprintf(os.Stderr, "discover: at least one name required")
|
||||
}
|
||||
|
||||
for _, name := range args {
|
||||
app, err := discovery.NewAppFromString(name)
|
||||
if err != nil {
|
||||
stderr("%s: %s", name, err)
|
||||
return 1
|
||||
}
|
||||
eps, err := discovery.DiscoverEndpoints(*app, transportFlags.Insecure)
|
||||
|
||||
if err != nil {
|
||||
stderr("error fetching %s: %s", name, err)
|
||||
return 1
|
||||
}
|
||||
for _, list := range [][]string{eps.Sig, eps.ACI, eps.Keys} {
|
||||
if len(list) != 0 {
|
||||
fmt.Println(strings.Join(list, ","))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
var (
|
||||
cmdHelp = &Command{
|
||||
Name: "help",
|
||||
Summary: "Show a list of commands or help for one command",
|
||||
Usage: "[COMMAND]",
|
||||
Description: "Show a list of commands or detailed help for one command",
|
||||
Run: runHelp,
|
||||
}
|
||||
|
||||
globalUsageTemplate *template.Template
|
||||
commandUsageTemplate *template.Template
|
||||
templFuncs = template.FuncMap{
|
||||
"descToLines": func(s string) []string {
|
||||
// trim leading/trailing whitespace and split into slice of lines
|
||||
return strings.Split(strings.Trim(s, "\n\t "), "\n")
|
||||
},
|
||||
"printOption": func(name, defvalue, usage string) string {
|
||||
prefix := "--"
|
||||
if len(name) == 1 {
|
||||
prefix = "-"
|
||||
}
|
||||
return fmt.Sprintf("\t%s%s=%s\t%s", prefix, name, defvalue, usage)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
globalUsageTemplate = template.Must(template.New("global_usage").Funcs(templFuncs).Parse(`
|
||||
NAME:
|
||||
{{printf "\t%s - %s" .Executable .Description}}
|
||||
|
||||
USAGE:
|
||||
{{printf "\t%s" .Executable}} [global options] <command> [command options] [arguments...]
|
||||
|
||||
VERSION:
|
||||
{{printf "\t%s" .Version}}
|
||||
|
||||
COMMANDS:{{range .Commands}}
|
||||
{{printf "\t%s\t%s" .Name .Summary}}{{end}}
|
||||
|
||||
GLOBAL OPTIONS:{{range .Flags}}
|
||||
{{printOption .Name .DefValue .Usage}}{{end}}
|
||||
|
||||
Run "{{.Executable}} help <command>" for more details on a specific command.
|
||||
`[1:]))
|
||||
commandUsageTemplate = template.Must(template.New("command_usage").Funcs(templFuncs).Parse(`
|
||||
NAME:
|
||||
{{printf "\t%s - %s" .Cmd.Name .Cmd.Summary}}
|
||||
|
||||
USAGE:
|
||||
{{printf "\t%s %s %s" .Executable .Cmd.Name .Cmd.Usage}}
|
||||
|
||||
DESCRIPTION:
|
||||
{{range $line := descToLines .Cmd.Description}}{{printf "\t%s" $line}}
|
||||
{{end}}
|
||||
{{if .CmdFlags}}OPTIONS:{{range .CmdFlags}}
|
||||
{{printOption .Name .DefValue .Usage}}{{end}}
|
||||
|
||||
{{end}}For help on global options run "{{.Executable}} help"
|
||||
`[1:]))
|
||||
}
|
||||
|
||||
func runHelp(args []string) (exit int) {
|
||||
if len(args) < 1 {
|
||||
printGlobalUsage()
|
||||
return
|
||||
}
|
||||
|
||||
var cmd *Command
|
||||
|
||||
for _, c := range commands {
|
||||
if c.Name == args[0] {
|
||||
cmd = c
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if cmd == nil {
|
||||
fmt.Fprintf(os.Stderr, "Unrecognized command: %s\n", args[0])
|
||||
return 1
|
||||
}
|
||||
|
||||
printCommandUsage(cmd)
|
||||
return
|
||||
}
|
||||
|
||||
func printGlobalUsage() {
|
||||
globalUsageTemplate.Execute(out, struct {
|
||||
Executable string
|
||||
Commands []*Command
|
||||
Flags []*flag.Flag
|
||||
Description string
|
||||
Version string
|
||||
}{
|
||||
cliName,
|
||||
commands,
|
||||
getAllFlags(),
|
||||
cliDescription,
|
||||
"0.1.0",
|
||||
})
|
||||
out.Flush()
|
||||
}
|
||||
|
||||
func printCommandUsage(cmd *Command) {
|
||||
commandUsageTemplate.Execute(out, struct {
|
||||
Executable string
|
||||
Cmd *Command
|
||||
CmdFlags []*flag.Flag
|
||||
}{
|
||||
cliName,
|
||||
cmd,
|
||||
getFlags(&cmd.Flags),
|
||||
})
|
||||
out.Flush()
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/bzip2"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/rocket/app-container/aci"
|
||||
"github.com/coreos/rocket/app-container/schema"
|
||||
)
|
||||
|
||||
const (
|
||||
typeAppImage = "appimage"
|
||||
typeImageLayout = "layout"
|
||||
typeManifest = "manifest"
|
||||
)
|
||||
|
||||
var (
|
||||
valType string
|
||||
cmdValidate = &Command{
|
||||
Name: "validate",
|
||||
Description: "Validate one or more AppContainer files",
|
||||
Summary: "Validate that one or more images or manifests meet the AppContainer specification",
|
||||
Usage: "[--type=TYPE] FILE...",
|
||||
Run: runValidate,
|
||||
}
|
||||
types = []string{
|
||||
typeAppImage,
|
||||
typeImageLayout,
|
||||
typeManifest,
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmdValidate.Flags.StringVar(&valType, "type", "",
|
||||
fmt.Sprintf(`Type of file to validate. If unset, actool will try to detect the type. One of "%s"`, strings.Join(types, ",")))
|
||||
}
|
||||
|
||||
func runValidate(args []string) (exit int) {
|
||||
if len(args) < 1 {
|
||||
stderr("must pass one or more files")
|
||||
return 1
|
||||
}
|
||||
|
||||
for _, path := range args {
|
||||
vt := valType
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
stderr("unable to access %s: %v", path, err)
|
||||
return 1
|
||||
}
|
||||
var fh *os.File
|
||||
if fi.IsDir() {
|
||||
switch vt {
|
||||
case typeImageLayout:
|
||||
case "":
|
||||
vt = typeImageLayout
|
||||
case typeManifest, typeAppImage:
|
||||
stderr("%s is a directory (wrong --type?)", path)
|
||||
return 1
|
||||
default:
|
||||
// should never happen
|
||||
panic(fmt.Sprintf("unexpected type: %v", vt))
|
||||
}
|
||||
} else {
|
||||
fh, err = os.Open(path)
|
||||
if err != nil {
|
||||
stderr("%s: unable to open: %v", path, err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
if vt == "" {
|
||||
vt, err = detectValType(fh)
|
||||
if err != nil {
|
||||
stderr("%s: error detecting file type: %v", path, err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
switch vt {
|
||||
case typeImageLayout:
|
||||
err = aci.ValidateLayout(path)
|
||||
if err != nil {
|
||||
stderr("%s: invalid image layout: %v", path, err)
|
||||
exit = 1
|
||||
} else if globalFlags.Debug {
|
||||
stderr("%s: valid image layout", path)
|
||||
}
|
||||
case typeAppImage:
|
||||
fr, err := maybeDecompress(fh)
|
||||
if err != nil {
|
||||
stderr("%s: error decompressing file: %v", path, err)
|
||||
return 1
|
||||
}
|
||||
tr := tar.NewReader(fr)
|
||||
err = aci.ValidateArchive(tr)
|
||||
fh.Close()
|
||||
if err != nil {
|
||||
stderr("%s: error validating: %v", path, err)
|
||||
exit = 1
|
||||
} else if globalFlags.Debug {
|
||||
stderr("%s: valid app container image", path)
|
||||
}
|
||||
case typeManifest:
|
||||
b, err := ioutil.ReadAll(fh)
|
||||
fh.Close()
|
||||
if err != nil {
|
||||
stderr("%s: unable to read file %s", path, err)
|
||||
return 1
|
||||
}
|
||||
k := schema.Kind{}
|
||||
if err := k.UnmarshalJSON(b); err != nil {
|
||||
stderr("%s: error unmarshaling manifest: %v", path, err)
|
||||
return 1
|
||||
}
|
||||
switch k.ACKind {
|
||||
case "ImageManifest":
|
||||
m := schema.ImageManifest{}
|
||||
err = m.UnmarshalJSON(b)
|
||||
case "ContainerRuntimeManifest":
|
||||
m := schema.ContainerRuntimeManifest{}
|
||||
err = m.UnmarshalJSON(b)
|
||||
default:
|
||||
// Should not get here; schema.Kind unmarshal should fail
|
||||
panic("bad ACKind")
|
||||
}
|
||||
if err != nil {
|
||||
stderr("%s: invalid %s: %v", path, k.ACKind, err)
|
||||
exit = 1
|
||||
} else if globalFlags.Debug {
|
||||
stderr("%s: valid %s", path, k.ACKind)
|
||||
}
|
||||
default:
|
||||
stderr("%s: unable to detect filetype (try --type)", path)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func detectValType(file *os.File) (string, error) {
|
||||
typ, err := aci.DetectFileType(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := file.Seek(0, 0); err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch typ {
|
||||
case aci.TypeXz, aci.TypeGzip, aci.TypeBzip2, aci.TypeTar:
|
||||
return typeAppImage, nil
|
||||
case aci.TypeText:
|
||||
return typeManifest, nil
|
||||
default:
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
func maybeDecompress(rs io.ReadSeeker) (io.Reader, error) {
|
||||
// TODO(jonboulle): this is a bit redundant with detectValType
|
||||
typ, err := aci.DetectFileType(rs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := rs.Seek(0, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var r io.Reader
|
||||
switch typ {
|
||||
case aci.TypeGzip:
|
||||
r, err = gzip.NewReader(rs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading gzip: %v", err)
|
||||
}
|
||||
case aci.TypeBzip2:
|
||||
r = bzip2.NewReader(rs)
|
||||
case aci.TypeXz:
|
||||
r = aci.XzReader(rs)
|
||||
case aci.TypeTar:
|
||||
r = rs
|
||||
case aci.TypeUnknown:
|
||||
return nil, errors.New("unknown filetype")
|
||||
default:
|
||||
// should never happen
|
||||
panic(fmt.Sprintf("bad type returned from DetectFileType: %v", typ))
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/coreos/rocket/app-container/schema"
|
||||
)
|
||||
|
||||
var cmdVersion = &Command{
|
||||
Name: "version",
|
||||
Description: "Print the version and exit",
|
||||
Summary: "Print the version and exit",
|
||||
Run: runVersion,
|
||||
}
|
||||
|
||||
func runVersion(args []string) (exit int) {
|
||||
fmt.Printf("actool version %s\n", schema.AppContainerVersion.String())
|
||||
return
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/rocket/Godeps/_workspace/src/golang.org/x/net/html"
|
||||
"github.com/coreos/rocket/Godeps/_workspace/src/golang.org/x/net/html/atom"
|
||||
)
|
||||
|
||||
type acMeta struct {
|
||||
name string
|
||||
prefix string
|
||||
uri string
|
||||
}
|
||||
|
||||
type Endpoints struct {
|
||||
Sig []string
|
||||
ACI []string
|
||||
Keys []string
|
||||
}
|
||||
|
||||
func appendMeta(meta []acMeta, attrs []html.Attribute) []acMeta {
|
||||
m := acMeta{}
|
||||
|
||||
for _, a := range attrs {
|
||||
if a.Namespace != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
switch a.Key {
|
||||
case "name":
|
||||
m.name = a.Val
|
||||
|
||||
case "content":
|
||||
parts := strings.SplitN(strings.TrimSpace(a.Val), " ", 2)
|
||||
if len(parts) < 2 {
|
||||
break
|
||||
}
|
||||
m.prefix = parts[0]
|
||||
m.uri = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(eyakubovich): should prefix be optional?
|
||||
if !strings.HasPrefix(m.name, "ac-") || m.prefix == "" || m.uri == "" {
|
||||
return meta
|
||||
}
|
||||
|
||||
return append(meta, m)
|
||||
}
|
||||
|
||||
func extractACMeta(r io.Reader) []acMeta {
|
||||
var meta []acMeta
|
||||
|
||||
z := html.NewTokenizer(r)
|
||||
|
||||
for {
|
||||
switch z.Next() {
|
||||
case html.ErrorToken:
|
||||
return meta
|
||||
|
||||
case html.StartTagToken, html.SelfClosingTagToken:
|
||||
tok := z.Token()
|
||||
if tok.DataAtom == atom.Meta {
|
||||
meta = appendMeta(meta, tok.Attr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func renderTemplate(tpl string, kvs ...string) string {
|
||||
for i := 0; i < len(kvs); i += 2 {
|
||||
k := kvs[i]
|
||||
v := kvs[i+1]
|
||||
tpl = strings.Replace(tpl, k, v, -1)
|
||||
}
|
||||
return tpl
|
||||
}
|
||||
|
||||
func DiscoverEndpoints(app App, insecure bool) (*Endpoints, error) {
|
||||
_, body, err := httpsOrHTTP(app.Name.String(), insecure)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
meta := extractACMeta(body)
|
||||
|
||||
tplVars := []string{"{os}", app.Labels["os"], "{arch}", app.Labels["arch"],
|
||||
"{name}", app.Name.String(), "{version}", app.Labels["version"]}
|
||||
|
||||
de := &Endpoints{}
|
||||
|
||||
for _, m := range meta {
|
||||
if !strings.HasPrefix(app.Name.String(), m.prefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch m.name {
|
||||
case "ac-discovery":
|
||||
m.uri = renderTemplate(m.uri, tplVars...)
|
||||
de.Sig = append(de.Sig, renderTemplate(m.uri, "{ext}", "sig"))
|
||||
de.ACI = append(de.ACI, renderTemplate(m.uri, "{ext}", "aci"))
|
||||
|
||||
case "ac-discovery-pubkeys":
|
||||
de.Keys = append(de.Keys, m.uri)
|
||||
}
|
||||
}
|
||||
|
||||
return de, nil
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func fakeHTTPGet(filename string) func(uri string) (*http.Response, error) {
|
||||
return func(uri string) (*http.Response, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &http.Response{
|
||||
Status: "200 OK",
|
||||
StatusCode: 200,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{"text/html"},
|
||||
},
|
||||
Body: f,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverEndpoints(t *testing.T) {
|
||||
httpGet = fakeHTTPGet("myapp.html")
|
||||
|
||||
a := App{
|
||||
Name: "example.com/myapp",
|
||||
Labels: map[string]string{
|
||||
"version": "1.0.0",
|
||||
"os": "linux",
|
||||
"arch": "amd64",
|
||||
},
|
||||
}
|
||||
de, err := DiscoverEndpoints(a, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(de.Sig) != 2 {
|
||||
t.Errorf("Sig array is wrong length")
|
||||
} else {
|
||||
if de.Sig[0] != "https://storage.example.com/example.com/myapp-1.0.0.sig?torrent" {
|
||||
t.Error("Sig[0] mismatch: ", de.Sig[0])
|
||||
}
|
||||
if de.Sig[1] != "hdfs://storage.example.com/example.com/myapp-1.0.0.sig" {
|
||||
t.Error("Sig[1] mismatch: ", de.Sig[0])
|
||||
}
|
||||
}
|
||||
|
||||
if len(de.ACI) != 2 {
|
||||
t.Errorf("ACI array is wrong length")
|
||||
} else {
|
||||
if de.ACI[0] != "https://storage.example.com/example.com/myapp-1.0.0.aci?torrent" {
|
||||
t.Error("ACI[0] mismatch: ", de.ACI[0])
|
||||
}
|
||||
if de.ACI[1] != "hdfs://storage.example.com/example.com/myapp-1.0.0.aci" {
|
||||
t.Error("ACI[1] mismatch: ", de.ACI[1])
|
||||
}
|
||||
}
|
||||
|
||||
if len(de.Keys) != 1 {
|
||||
t.Errorf("Keys array is wrong length")
|
||||
} else {
|
||||
if de.Keys[0] != "https://example.com/pubkeys.gpg" {
|
||||
t.Error("Keys[0] mismatch: ", de.Keys[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
var httpGet = http.Get
|
||||
|
||||
func httpsOrHTTP(name string, insecure bool) (urlStr string, body io.ReadCloser, err error) {
|
||||
fetch := func(scheme string) (urlStr string, res *http.Response, err error) {
|
||||
u, err := url.Parse(scheme + "://" + name)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
u.RawQuery = "ac-discovery=1"
|
||||
urlStr = u.String()
|
||||
res, err = httpGet(urlStr)
|
||||
return
|
||||
}
|
||||
closeBody := func(res *http.Response) {
|
||||
if res != nil {
|
||||
res.Body.Close()
|
||||
}
|
||||
}
|
||||
urlStr, res, err := fetch("https")
|
||||
if err != nil || res.StatusCode != 200 {
|
||||
closeBody(res)
|
||||
if insecure {
|
||||
urlStr, res, err = fetch("http")
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
closeBody(res)
|
||||
return "", nil, err
|
||||
}
|
||||
return urlStr, res.Body, nil
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>My app</title>
|
||||
<meta name="ac-discovery" content="example.com https://storage.example.com/{name}-{version}.{ext}?torrent">
|
||||
<meta name="ac-discovery" content="example.com hdfs://storage.example.com/{name}-{version}.{ext}">
|
||||
<meta name="ac-discovery-cas-prefix" content="example.com https://storage.example.com/cas/">
|
||||
<meta name="ac-discovery-pubkeys" content="example.com https://example.com/pubkeys.gpg">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>My App</h1>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,80 +0,0 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/rocket/app-container/schema/types"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultVersion = "latest"
|
||||
defaultOS = runtime.GOOS
|
||||
defaultArch = runtime.GOARCH
|
||||
)
|
||||
|
||||
type App struct {
|
||||
Name types.ACName
|
||||
Labels map[string]string
|
||||
}
|
||||
|
||||
func NewApp(name string, labels map[string]string) (*App, error) {
|
||||
if labels == nil {
|
||||
labels = make(map[string]string, 0)
|
||||
}
|
||||
acn, err := types.NewACName(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &App{
|
||||
Name: *acn,
|
||||
Labels: labels,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewAppFromString takes a command line app parameter and returns a map of labels.
|
||||
//
|
||||
// Example app parameters:
|
||||
// example.com/reduce-worker:1.0.0
|
||||
// example.com/reduce-worker,channel=alpha,label=value
|
||||
func NewAppFromString(app string) (*App, error) {
|
||||
var (
|
||||
name string
|
||||
labels map[string]string
|
||||
)
|
||||
|
||||
app = strings.Replace(app, ":", ",version=", -1)
|
||||
app = "name=" + app
|
||||
v, err := url.ParseQuery(strings.Replace(app, ",", "&", -1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
labels = make(map[string]string, 0)
|
||||
for key, val := range v {
|
||||
if len(val) > 1 {
|
||||
return nil, fmt.Errorf("label %s with multiple values %q", key, val)
|
||||
}
|
||||
if key == "name" {
|
||||
name = val[0]
|
||||
continue
|
||||
}
|
||||
labels[key] = val[0]
|
||||
}
|
||||
if labels["version"] == "" {
|
||||
labels["version"] = defaultVersion
|
||||
}
|
||||
if labels["os"] == "" {
|
||||
labels["os"] = defaultOS
|
||||
}
|
||||
if labels["arch"] == "" {
|
||||
labels["arch"] = defaultArch
|
||||
}
|
||||
|
||||
a, err := NewApp(name, labels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
{
|
||||
"acKind": "ImageManifest",
|
||||
"acVersion": "0.1.0",
|
||||
"name": "example.com/reduce-worker",
|
||||
"labels": [
|
||||
{
|
||||
"name": "version",
|
||||
"val": "1.0.0"
|
||||
},
|
||||
{
|
||||
"name": "arch",
|
||||
"val": "amd64"
|
||||
},
|
||||
{
|
||||
"name": "os",
|
||||
"val": "linux"
|
||||
}
|
||||
],
|
||||
"app": {
|
||||
"exec": [
|
||||
"/usr/bin/reduce-worker"
|
||||
],
|
||||
"user": "100",
|
||||
"group": "300",
|
||||
"eventHandlers": [
|
||||
{
|
||||
"exec": [
|
||||
"/usr/bin/data-downloader"
|
||||
],
|
||||
"name": "pre-start"
|
||||
},
|
||||
{
|
||||
"exec": [
|
||||
"/usr/bin/deregister-worker"
|
||||
],
|
||||
"name": "post-stop"
|
||||
}
|
||||
],
|
||||
"environment": {
|
||||
"REDUCE_WORKER_DEBUG": "true"
|
||||
},
|
||||
"isolators": [
|
||||
{
|
||||
"name": "private-network",
|
||||
"val": "true"
|
||||
},
|
||||
{
|
||||
"name": "cpu/shares",
|
||||
"val": "20"
|
||||
},
|
||||
{
|
||||
"name": "memory/limit",
|
||||
"val": "1G"
|
||||
},
|
||||
{
|
||||
"name": "capabilities/bounding-set",
|
||||
"val": "CAP_NET_BIND_SERVICECAP_SYS_ADMIN"
|
||||
}
|
||||
],
|
||||
"mountPoints": [
|
||||
{
|
||||
"name": "database",
|
||||
"path": "/var/lib/db",
|
||||
"readOnly": false
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
{
|
||||
"name": "health",
|
||||
"port": 4000,
|
||||
"protocol": "tcp",
|
||||
"socketActivated": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"dependencies": [
|
||||
{
|
||||
"hash": "sha256-...",
|
||||
"labels": [
|
||||
{
|
||||
"name": "os",
|
||||
"val": "linux"
|
||||
},
|
||||
{
|
||||
"name": "env",
|
||||
"val": "canary"
|
||||
}
|
||||
],
|
||||
"name": "example.com/reduce-worker-base",
|
||||
"root": "/"
|
||||
}
|
||||
],
|
||||
"pathWhitelist": [
|
||||
"/etc/ca/example.com/crt",
|
||||
"/usr/bin/map-reduce-worker",
|
||||
"/opt/libs/reduce-toolkit.so",
|
||||
"/etc/reduce-worker.conf",
|
||||
"/etc/systemd/system/"
|
||||
],
|
||||
"annotations": {
|
||||
"authors": "Carly Container <carly@example.com>, Nat Network <[nat@example.com](mailto:nat@example.com)>",
|
||||
"created": "2014-10-27T19:32:27.67021798Z",
|
||||
"documentation": "https://example.com/docs",
|
||||
"homepage": "https://example.com"
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
{
|
||||
"acVersion": "1.0.0",
|
||||
"acKind": "ContainerRuntimeManifest",
|
||||
"uuid": "6733C088-A507-4694-AABF-EDBE4FC5266F",
|
||||
"apps": [
|
||||
{
|
||||
"app": "example.com/reduce-worker-1.0.0",
|
||||
"imageID": "sha256-908540d22dae9d8e6e3c6b13e21ddd12817406fd5c948eae4a744a6ccf94f96d"
|
||||
},
|
||||
{
|
||||
"app": "example.com/worker-backup-1.0.0",
|
||||
"imageID": "sha256-893e424371071a51a45ebf490e852dfb1354b633f0817075d3bae80a6bdbafb1",
|
||||
"isolators": [
|
||||
{
|
||||
"name": "memory/limit",
|
||||
"val": "1G"
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"foo": "baz"
|
||||
}
|
||||
},
|
||||
{
|
||||
"app": "example.com/reduce-worker-register-1.0.0",
|
||||
"imageID": "sha256-f11cc60e67aeec90031cd17582327ee7e918d1c18d6b82eba8997df7410ead8d"
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"kind": "host",
|
||||
"source": "/opt/tenant1/database",
|
||||
"readOnly": true,
|
||||
"fulfills": [
|
||||
"database"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "empty",
|
||||
"fulfills": [
|
||||
"buildoutput"
|
||||
]
|
||||
}
|
||||
],
|
||||
"isolators": [
|
||||
{
|
||||
"name": "memory/limit",
|
||||
"val": "4G"
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"ipAddress": "10.1.2.3"
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/coreos/rocket/app-container/schema/types"
|
||||
)
|
||||
|
||||
const (
|
||||
ACIExtension = ".aci"
|
||||
)
|
||||
|
||||
type ImageManifest struct {
|
||||
ACKind types.ACKind `json:"acKind"`
|
||||
ACVersion types.SemVer `json:"acVersion"`
|
||||
Name types.ACName `json:"name"`
|
||||
Labels types.Labels `json:"labels"`
|
||||
App types.App `json:"app"`
|
||||
Annotations types.Annotations `json:"annotations"`
|
||||
}
|
||||
|
||||
// appManifest is a model to facilitate extra validation during the
|
||||
// unmarshalling of the ImageManifest
|
||||
type appManifest ImageManifest
|
||||
|
||||
func (am *ImageManifest) UnmarshalJSON(data []byte) error {
|
||||
a := appManifest{}
|
||||
err := json.Unmarshal(data, &a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nam := ImageManifest(a)
|
||||
if err := nam.assertValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
*am = nam
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am ImageManifest) MarshalJSON() ([]byte, error) {
|
||||
if err := am.assertValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(appManifest(am))
|
||||
}
|
||||
|
||||
// assertValid performs extra assertions on an ImageManifest to ensure that
|
||||
// fields are set appropriately, etc. It is used exclusively when marshalling
|
||||
// and unmarshalling an ImageManifest. Most field-specific validation is
|
||||
// performed through the individual types being marshalled; assertValid()
|
||||
// should only deal with higher-level validation.
|
||||
func (am *ImageManifest) assertValid() error {
|
||||
if am.ACKind != "ImageManifest" {
|
||||
return types.ACKindError(`missing or bad ACKind (must be "ImageManifest")`)
|
||||
}
|
||||
if am.ACVersion.Empty() {
|
||||
return errors.New(`acVersion must be set`)
|
||||
}
|
||||
if am.Name.Empty() {
|
||||
return errors.New(`name must be set`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/coreos/rocket/app-container/schema/types"
|
||||
)
|
||||
|
||||
type ContainerRuntimeManifest struct {
|
||||
ACVersion types.SemVer `json:"acVersion"`
|
||||
ACKind types.ACKind `json:"acKind"`
|
||||
UUID types.UUID `json:"uuid"`
|
||||
Apps AppList `json:"apps"`
|
||||
Volumes []types.Volume `json:"volumes"`
|
||||
Isolators []types.Isolator `json:"isolators"`
|
||||
Annotations map[types.ACName]string `json:"annotations"`
|
||||
}
|
||||
|
||||
// containerRuntimeManifest is a model to facilitate extra validation during the
|
||||
// unmarshalling of the ContainerRuntimeManifest
|
||||
type containerRuntimeManifest ContainerRuntimeManifest
|
||||
|
||||
func (cm *ContainerRuntimeManifest) UnmarshalJSON(data []byte) error {
|
||||
c := containerRuntimeManifest{}
|
||||
err := json.Unmarshal(data, &c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ncm := ContainerRuntimeManifest(c)
|
||||
if err := ncm.assertValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
*cm = ncm
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cm ContainerRuntimeManifest) MarshalJSON() ([]byte, error) {
|
||||
if err := cm.assertValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(containerRuntimeManifest(cm))
|
||||
}
|
||||
|
||||
// assertValid performs extra assertions on an ContainerRuntimeManifest to
|
||||
// ensure that fields are set appropriately, etc. It is used exclusively when
|
||||
// marshalling and unmarshalling an ContainerRuntimeManifest. Most
|
||||
// field-specific validation is performed through the individual types being
|
||||
// marshalled; assertValid() should only deal with higher-level validation.
|
||||
func (cm *ContainerRuntimeManifest) assertValid() error {
|
||||
if cm.ACKind != "ContainerRuntimeManifest" {
|
||||
return types.ACKindError(`missing or bad ACKind (must be "ContainerRuntimeManifest")`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AppList []RuntimeApp
|
||||
|
||||
// Get retrieves an app by the specified name from the AppList; if there is
|
||||
// no such app, nil is returned. The returned *RuntimeApp MUST be considered
|
||||
// read-only.
|
||||
func (al AppList) Get(name types.ACName) *RuntimeApp {
|
||||
for _, a := range al {
|
||||
if name.Equals(a.Name) {
|
||||
aa := a
|
||||
return &aa
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RuntimeApp describes an application referenced in a ContainerRuntimeManifest
|
||||
type RuntimeApp struct {
|
||||
Name types.ACName `json:"name"`
|
||||
ImageID types.Hash `json:"imageID"`
|
||||
Isolators []types.Isolator `json:"isolators"`
|
||||
Annotations map[types.ACName]string `json:"annotations"`
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package schema
|
||||
|
||||
/*
|
||||
|
||||
Package schema provides definitions for the JSON schema of the different
|
||||
manifests in the App Container Standard. The manifests are canonically
|
||||
represented in their respective structs:
|
||||
- `ImageManifest`
|
||||
- `ContainerRuntimeManifest`
|
||||
|
||||
Validation is performed through serialization: if a blob of JSON data will
|
||||
unmarshal to one of the *Manifests, it is considered a valid implementation
|
||||
of the standard. Similarly, if a constructed *Manifest struct marshals
|
||||
successfully to JSON, it must be valid.
|
||||
|
||||
*/
|
||||
@@ -1,28 +0,0 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/coreos/rocket/app-container/schema/types"
|
||||
)
|
||||
|
||||
type Kind struct {
|
||||
ACVersion types.SemVer `json:"acVersion"`
|
||||
ACKind types.ACKind `json:"acKind"`
|
||||
}
|
||||
|
||||
type kind Kind
|
||||
|
||||
func (k *Kind) UnmarshalJSON(data []byte) error {
|
||||
nk := kind{}
|
||||
err := json.Unmarshal(data, &nk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*k = Kind(nk)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k Kind) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(kind(k))
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoACKind = ACKindError("ACKind must be set")
|
||||
)
|
||||
|
||||
// ACKind wraps a string to define a field which must be set with one of
|
||||
// several ACKind values. If it is unset, or has an invalid value, the field
|
||||
// will refuse to marshal/unmarshal.
|
||||
type ACKind string
|
||||
|
||||
func (a ACKind) String() string {
|
||||
return string(a)
|
||||
}
|
||||
|
||||
func (a ACKind) assertValid() error {
|
||||
s := a.String()
|
||||
switch s {
|
||||
case "ImageManifest", "ContainerRuntimeManifest":
|
||||
return nil
|
||||
case "":
|
||||
return ErrNoACKind
|
||||
default:
|
||||
msg := fmt.Sprintf("bad ACKind: %s", s)
|
||||
return ACKindError(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (a ACKind) MarshalJSON() ([]byte, error) {
|
||||
if err := a.assertValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(a.String())
|
||||
}
|
||||
|
||||
func (a *ACKind) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
err := json.Unmarshal(data, &s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
na := ACKind(s)
|
||||
if err := na.assertValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
*a = na
|
||||
return nil
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestACKindMarshalBad(t *testing.T) {
|
||||
tests := map[string]error{
|
||||
"Foo": ACKindError("bad ACKind: Foo"),
|
||||
"ApplicationManifest": ACKindError("bad ACKind: ApplicationManifest"),
|
||||
"": ErrNoACKind,
|
||||
}
|
||||
for in, werr := range tests {
|
||||
a := ACKind(in)
|
||||
b, gerr := json.Marshal(a)
|
||||
if b != nil {
|
||||
t.Errorf("ACKind(%q): want b=nil, got %v", in, b)
|
||||
}
|
||||
if jerr, ok := gerr.(*json.MarshalerError); !ok {
|
||||
t.Errorf("expected JSONMarshalerError")
|
||||
} else {
|
||||
if e := jerr.Err; e != werr {
|
||||
t.Errorf("err=%#v, want %#v", e, werr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestACKindMarshalGood(t *testing.T) {
|
||||
for i, in := range []string{
|
||||
"ImageManifest",
|
||||
"ContainerRuntimeManifest",
|
||||
} {
|
||||
a := ACKind(in)
|
||||
b, err := json.Marshal(a)
|
||||
if !reflect.DeepEqual(b, []byte(`"`+in+`"`)) {
|
||||
t.Errorf("#%d: marshalled=%v, want %v", i, b, []byte(in))
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("#%d: err=%v, want nil", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
valchars = `abcdefghijklmnopqrstuvwxyz0123456789.-/`
|
||||
)
|
||||
|
||||
// ACName (an App-Container Name) is a format used by keys in different
|
||||
// formats of the App Container Standard. An ACName is restricted to
|
||||
// characters accepted by the DNS RFC[1] and "/". ACNames are
|
||||
// case-insensitive for comparison purposes, but case-preserving.
|
||||
//
|
||||
// [1] http://tools.ietf.org/html/rfc1123#page-13
|
||||
type ACName string
|
||||
|
||||
func (n ACName) String() string {
|
||||
return string(n)
|
||||
}
|
||||
|
||||
// Equals checks whether a given ACName is equal to this one.
|
||||
func (n ACName) Equals(o ACName) bool {
|
||||
return strings.ToLower(string(n)) == strings.ToLower(string(o))
|
||||
}
|
||||
|
||||
func (n ACName) Empty() bool {
|
||||
return n.String() == ""
|
||||
}
|
||||
|
||||
// NewACName generates a new ACName from a string. If the given string is
|
||||
// not a valid ACName, nil and an error are returned.
|
||||
func NewACName(s string) (*ACName, error) {
|
||||
if len(s) == 0 {
|
||||
return nil, fmt.Errorf("ACName cannot be empty")
|
||||
}
|
||||
for _, c := range s {
|
||||
if !strings.ContainsRune(valchars, c) {
|
||||
msg := fmt.Sprintf("invalid char in ACName: %c", c)
|
||||
return nil, ACNameError(msg)
|
||||
}
|
||||
}
|
||||
return (*ACName)(&s), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface
|
||||
func (n *ACName) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
nn, err := NewACName(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*n = *nn
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface
|
||||
func (n *ACName) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(n.String())
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package types
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNewACName(t *testing.T) {
|
||||
tests := []string{
|
||||
"asdf",
|
||||
"foo-bar-baz",
|
||||
"database",
|
||||
"example.com/database",
|
||||
"example.com/ourapp-1.0.0",
|
||||
"sub-domain.example.com/org/product/release-1.0.0",
|
||||
}
|
||||
for i, in := range tests {
|
||||
l, err := NewACName(in)
|
||||
if err != nil {
|
||||
t.Errorf("#%d: got err=%v, want nil", i, err)
|
||||
}
|
||||
if l == nil {
|
||||
t.Errorf("#%d: got l=nil, want non-nil", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewACNameBad(t *testing.T) {
|
||||
tests := []string{
|
||||
"",
|
||||
"foo#",
|
||||
"EXAMPLE.com",
|
||||
"foo.com/BAR",
|
||||
"example.com/app_1",
|
||||
}
|
||||
for i, in := range tests {
|
||||
l, err := NewACName(in)
|
||||
if l != nil {
|
||||
t.Errorf("#%d: got l=%v, want nil", i, l)
|
||||
}
|
||||
if err == nil {
|
||||
t.Errorf("#%d: got err=nil, want non-nil", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// TODO(jonboulle): this is awkward since it's inconsistent with the way we do
|
||||
// things elsewhere (i.e. using strict types instead of string types), but it's
|
||||
// tricky because Annotations needs to be able to catch arbitrary key-values.
|
||||
// Clean this up somehow?
|
||||
type Annotations map[ACName]string
|
||||
|
||||
type annotations Annotations
|
||||
|
||||
func (a Annotations) assertValid() error {
|
||||
if c, ok := a["created"]; ok {
|
||||
if _, err := NewDate(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if h, ok := a["homepage"]; ok {
|
||||
if _, err := NewURL(h); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if d, ok := a["documentation"]; ok {
|
||||
if _, err := NewURL(d); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a Annotations) MarshalJSON() ([]byte, error) {
|
||||
if err := a.assertValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(annotations(a))
|
||||
}
|
||||
|
||||
func (a *Annotations) UnmarshalJSON(data []byte) error {
|
||||
var ja annotations
|
||||
if err := json.Unmarshal(data, &ja); err != nil {
|
||||
return err
|
||||
}
|
||||
na := Annotations(ja)
|
||||
if err := a.assertValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
*a = na
|
||||
return nil
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
Exec []string `json:"exec"`
|
||||
EventHandlers []EventHandler `json:"eventHandlers"`
|
||||
User string `json:"user"`
|
||||
Group string `json:"group"`
|
||||
Environment map[string]string `json:"environment"`
|
||||
MountPoints []MountPoint `json:"mountPoints"`
|
||||
Ports []Port `json:"ports"`
|
||||
Isolators []Isolator `json:"isolators"`
|
||||
}
|
||||
|
||||
// app is a model to facilitate extra validation during the
|
||||
// unmarshalling of the App
|
||||
type app App
|
||||
|
||||
func (a *App) UnmarshalJSON(data []byte) error {
|
||||
ja := app{}
|
||||
err := json.Unmarshal(data, &ja)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
na := App(ja)
|
||||
if err := na.assertValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
if na.Environment == nil {
|
||||
na.Environment = make(map[string]string)
|
||||
}
|
||||
*a = na
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a App) MarshalJSON() ([]byte, error) {
|
||||
if err := a.assertValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(app(a))
|
||||
}
|
||||
|
||||
func (a *App) assertValid() error {
|
||||
if len(a.Exec) < 1 {
|
||||
return errors.New(`Exec cannot be empty`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Date wraps time.Time to marshal/unmarshal to/from JSON strings in strict
|
||||
// accordance with RFC3339
|
||||
// TODO(jonboulle): golang's implementation seems slightly buggy here;
|
||||
// according to http://tools.ietf.org/html/rfc3339#section-5.6 , applications
|
||||
// may choose to separate the date and time with a space instead of a T
|
||||
// character (for example, `date --rfc-3339` on GNU coreutils) - but this is
|
||||
// considered an error by go's parser. File a bug?
|
||||
type Date time.Time
|
||||
|
||||
func NewDate(s string) (*Date, error) {
|
||||
t, err := time.Parse(time.RFC3339, s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bad Date: %v", err)
|
||||
}
|
||||
d := Date(t)
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
func (d Date) String() string {
|
||||
return time.Time(d).Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func (d *Date) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
nd, err := NewDate(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*d = *nd
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d Date) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(d.String())
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
pst = time.FixedZone("Pacific", -8*60*60)
|
||||
)
|
||||
|
||||
func TestUnmarshalDate(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
|
||||
wt time.Time
|
||||
}{
|
||||
{
|
||||
`"2004-05-14T23:11:14+00:00"`,
|
||||
|
||||
time.Date(2004, 05, 14, 23, 11, 14, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
`"2001-02-03T04:05:06Z"`,
|
||||
|
||||
time.Date(2001, 02, 03, 04, 05, 06, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
`"2014-11-14T17:36:54-08:00"`,
|
||||
|
||||
time.Date(2014, 11, 14, 17, 36, 54, 0, pst),
|
||||
},
|
||||
{
|
||||
`"2004-05-14T23:11:14+00:00"`,
|
||||
|
||||
time.Date(2004, 05, 14, 23, 11, 14, 0, time.UTC),
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
var d Date
|
||||
if err := json.Unmarshal([]byte(tt.in), &d); err != nil {
|
||||
t.Errorf("#%d: got err=%v, want nil", i, err)
|
||||
}
|
||||
if gt := time.Time(d); !gt.Equal(tt.wt) {
|
||||
t.Errorf("#%d: got time=%v, want %v", i, gt, tt.wt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalDateBad(t *testing.T) {
|
||||
tests := []string{
|
||||
`not a json string`,
|
||||
`2014-11-14T17:36:54-08:00`,
|
||||
`"garbage"`,
|
||||
`"1416015188"`,
|
||||
`"Fri Nov 14 17:53:02 PST 2014"`,
|
||||
`"2014-11-1417:36:54"`,
|
||||
}
|
||||
for i, tt := range tests {
|
||||
var d Date
|
||||
if err := json.Unmarshal([]byte(tt), &d); err == nil {
|
||||
t.Errorf("#%d: unexpected nil err", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package types
|
||||
|
||||
// An ACKindError is returned when the wrong ACKind is set in a manifest
|
||||
type ACKindError string
|
||||
|
||||
func (e ACKindError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
// An ACVersionError is returned when a bad ACVersion is set in a manifest
|
||||
type ACVersionError string
|
||||
|
||||
func (e ACVersionError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
// An ACNameError is returned when a bad value is used for an ACName
|
||||
type ACNameError string
|
||||
|
||||
func (e ACNameError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
// An AMStartedOnError is returned when the wrong StartedOn is set in an ImageManifest
|
||||
type AMStartedOnError string
|
||||
|
||||
func (e AMStartedOnError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type EventHandler struct {
|
||||
Name string `json:"name"`
|
||||
Exec []string `json:"exec"`
|
||||
}
|
||||
|
||||
type eventHandler EventHandler
|
||||
|
||||
func (e EventHandler) assertValid() error {
|
||||
s := e.Name
|
||||
switch s {
|
||||
case "pre-start", "post-stop":
|
||||
return nil
|
||||
case "":
|
||||
return errors.New(`eventHandler "name" cannot be empty`)
|
||||
default:
|
||||
return fmt.Errorf(`bad eventHandler "name": %q`, s)
|
||||
}
|
||||
}
|
||||
|
||||
func (e EventHandler) MarshalJSON() ([]byte, error) {
|
||||
if err := e.assertValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(eventHandler(e))
|
||||
}
|
||||
|
||||
func (e *EventHandler) UnmarshalJSON(data []byte) error {
|
||||
var je eventHandler
|
||||
err := json.Unmarshal(data, &je)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ne := EventHandler(je)
|
||||
if err := ne.assertValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
*e = ne
|
||||
return nil
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Hash encodes a hash specified in a string of the form:
|
||||
// "<type>-<value>"
|
||||
// for example
|
||||
// "sha256-06c733b1838136838e6d2d3e8fa5aea4c7905e92"
|
||||
// Valid types are currently "sha256"
|
||||
type Hash struct {
|
||||
typ string
|
||||
Val string
|
||||
}
|
||||
|
||||
func NewHash(s string) (*Hash, error) {
|
||||
elems := strings.Split(s, "-")
|
||||
if len(elems) != 2 {
|
||||
return nil, errors.New("badly formatted hash string")
|
||||
}
|
||||
nh := Hash{
|
||||
typ: elems[0],
|
||||
Val: elems[1],
|
||||
}
|
||||
if err := nh.assertValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &nh, nil
|
||||
}
|
||||
|
||||
func (h Hash) String() string {
|
||||
return fmt.Sprintf("%s-%s", h.typ, h.Val)
|
||||
}
|
||||
|
||||
func (h Hash) assertValid() error {
|
||||
switch h.typ {
|
||||
case "sha256":
|
||||
case "":
|
||||
return fmt.Errorf("unexpected empty hash type")
|
||||
default:
|
||||
return fmt.Errorf("unrecognized hash type: %v", h.typ)
|
||||
}
|
||||
if h.Val == "" {
|
||||
return fmt.Errorf("unexpected empty hash value")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Hash) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
nh, err := NewHash(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*h = *nh
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h Hash) MarshalJSON() ([]byte, error) {
|
||||
if err := h.assertValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(h.String())
|
||||
}
|
||||
|
||||
func NewHashSHA256(b []byte) *Hash {
|
||||
h := sha256.New()
|
||||
h.Write(b)
|
||||
nh, _ := NewHash(fmt.Sprintf("sha256-%x", h.Sum(nil)))
|
||||
return nh
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMarshalHash(t *testing.T) {
|
||||
tests := []struct {
|
||||
typ string
|
||||
val string
|
||||
|
||||
wout string
|
||||
}{
|
||||
{
|
||||
"sha256",
|
||||
"abcdefghi",
|
||||
|
||||
`"sha256-abcdefghi"`,
|
||||
},
|
||||
{
|
||||
"sha256",
|
||||
"06c733b1838136838e6d2d3e8fa5aea4c7905e92",
|
||||
|
||||
`"sha256-06c733b1838136838e6d2d3e8fa5aea4c7905e92"`,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
h := Hash{
|
||||
typ: tt.typ,
|
||||
Val: tt.val,
|
||||
}
|
||||
b, err := json.Marshal(h)
|
||||
if err != nil {
|
||||
t.Errorf("#%d: unexpected err=%v", i, err)
|
||||
}
|
||||
if g := string(b); g != tt.wout {
|
||||
t.Errorf("#%d: got string=%v, want %v", i, g, tt.wout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalHashBad(t *testing.T) {
|
||||
tests := []struct {
|
||||
typ string
|
||||
val string
|
||||
}{
|
||||
{
|
||||
// empty value
|
||||
"sha256",
|
||||
"",
|
||||
},
|
||||
{
|
||||
// bad type
|
||||
"sha1",
|
||||
"abcdef",
|
||||
},
|
||||
{
|
||||
// empty type
|
||||
"",
|
||||
"abcdef",
|
||||
},
|
||||
{
|
||||
// empty empty
|
||||
"",
|
||||
"",
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
h := Hash{
|
||||
typ: tt.typ,
|
||||
Val: tt.val,
|
||||
}
|
||||
g, err := json.Marshal(h)
|
||||
if err == nil {
|
||||
t.Errorf("#%d: unexpected nil err", i)
|
||||
}
|
||||
if g != nil {
|
||||
t.Errorf("#%d: unexpected non-nil bytes: %v", i, g)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package types
|
||||
|
||||
type Isolator struct {
|
||||
Name ACName `json:"name"`
|
||||
Val string `json:"val"`
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// TODO(jonboulle): this is awkward since it's inconsistent with the way we do
|
||||
// things elsewhere (i.e. using strict types instead of string types), but it's
|
||||
// tricky because Labels needs to be able to catch arbitrary key-values.
|
||||
// Clean this up somehow?
|
||||
type Labels []Label
|
||||
|
||||
type labels Labels
|
||||
|
||||
type Label struct {
|
||||
Name ACName `json:"name"`
|
||||
Value string `json:"val"`
|
||||
}
|
||||
|
||||
func (l Labels) assertValid() error {
|
||||
if os, ok := l.get("os"); ok && os != "linux" {
|
||||
return errors.New(`bad os (must be "linux")`)
|
||||
}
|
||||
if arch, ok := l.get("arch"); ok && arch != "amd64" {
|
||||
return errors.New(`bad arch (must be "amd64")`)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l Labels) MarshalJSON() ([]byte, error) {
|
||||
if err := l.assertValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(labels(l))
|
||||
}
|
||||
|
||||
func (l *Labels) UnmarshalJSON(data []byte) error {
|
||||
var jl labels
|
||||
if err := json.Unmarshal(data, &jl); err != nil {
|
||||
return err
|
||||
}
|
||||
nl := Labels(jl)
|
||||
if err := l.assertValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
*l = nl
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l Labels) get(name string) (val string, ok bool) {
|
||||
for _, lbl := range l {
|
||||
if lbl.Name.String() == name {
|
||||
return lbl.Value, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package types
|
||||
|
||||
type MountPoint struct {
|
||||
Name ACName `json:"name"`
|
||||
Path string `json:"path"`
|
||||
ReadOnly bool `json:"readOnly"`
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package types
|
||||
|
||||
type Port struct {
|
||||
Name ACName `json:"name"`
|
||||
Protocol string `json:"protocol"`
|
||||
Port uint `json:"port"`
|
||||
SocketActivated bool `json:"socketActivated"`
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/coreos/rocket/Godeps/_workspace/src/github.com/coreos/go-semver/semver"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoZeroSemVer = ACVersionError("SemVer cannot be zero")
|
||||
ErrBadSemVer = ACVersionError("SemVer is bad")
|
||||
)
|
||||
|
||||
// SemVer implements the Unmarshaler interface to define a field that must be
|
||||
// a semantic version string
|
||||
// TODO(jonboulle): extend upstream instead of wrapping?
|
||||
type SemVer semver.Version
|
||||
|
||||
// NewSemVer generates a new SemVer from a string. If the given string does
|
||||
// not represent a valid SemVer, nil and an error are returned.
|
||||
func NewSemVer(s string) (*SemVer, error) {
|
||||
nsv, err := semver.NewVersion(s)
|
||||
if err != nil {
|
||||
return nil, ErrBadSemVer
|
||||
}
|
||||
v := SemVer(*nsv)
|
||||
if v.Empty() {
|
||||
return nil, ErrNoZeroSemVer
|
||||
}
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
func (sv SemVer) String() string {
|
||||
s := semver.Version(sv)
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func (sv SemVer) Empty() bool {
|
||||
return semver.Version(sv) == semver.Version{}
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface
|
||||
func (sv *SemVer) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
v, err := NewSemVer(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*sv = *v
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface
|
||||
func (sv SemVer) MarshalJSON() ([]byte, error) {
|
||||
if sv.Empty() {
|
||||
return nil, ErrNoZeroSemVer
|
||||
}
|
||||
return json.Marshal(sv.String())
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/rocket/Godeps/_workspace/src/github.com/coreos/go-semver/semver"
|
||||
)
|
||||
|
||||
func TestMarshalSemver(t *testing.T) {
|
||||
tests := []struct {
|
||||
sv SemVer
|
||||
|
||||
wd []byte
|
||||
}{
|
||||
{
|
||||
SemVer(semver.Version{Major: 1}),
|
||||
|
||||
[]byte(`"1.0.0"`),
|
||||
},
|
||||
{
|
||||
SemVer(semver.Version{Major: 3, Minor: 2, Patch: 1}),
|
||||
|
||||
[]byte(`"3.2.1"`),
|
||||
},
|
||||
{
|
||||
SemVer(semver.Version{Major: 3, Minor: 2, Patch: 1, PreRelease: "foo"}),
|
||||
|
||||
[]byte(`"3.2.1-foo"`),
|
||||
},
|
||||
{
|
||||
SemVer(semver.Version{Major: 1, Minor: 2, Patch: 3, PreRelease: "alpha", Metadata: "git"}),
|
||||
|
||||
[]byte(`"1.2.3-alpha+git"`),
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
d, err := json.Marshal(tt.sv)
|
||||
if !reflect.DeepEqual(d, tt.wd) {
|
||||
t.Errorf("#%d: d=%v, want %v", i, string(d), string(tt.wd))
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("#%d: err=%v, want nil", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalSemver(t *testing.T) {
|
||||
tests := []struct {
|
||||
d []byte
|
||||
|
||||
wsv SemVer
|
||||
werr bool
|
||||
}{
|
||||
{
|
||||
[]byte(`"1.0.0"`),
|
||||
|
||||
SemVer(semver.Version{Major: 1}),
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]byte(`"3.2.1"`),
|
||||
SemVer(semver.Version{Major: 3, Minor: 2, Patch: 1}),
|
||||
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]byte(`"3.2.1-foo"`),
|
||||
|
||||
SemVer(semver.Version{Major: 3, Minor: 2, Patch: 1, PreRelease: "foo"}),
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]byte(`"1.2.3-alpha+git"`),
|
||||
|
||||
SemVer(semver.Version{Major: 1, Minor: 2, Patch: 3, PreRelease: "alpha", Metadata: "git"}),
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]byte(`"1"`),
|
||||
|
||||
SemVer{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]byte(`"1.2.3.4"`),
|
||||
|
||||
SemVer{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]byte(`1.2.3`),
|
||||
|
||||
SemVer{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]byte(`"v1.2.3"`),
|
||||
|
||||
SemVer{},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
var sv SemVer
|
||||
err := json.Unmarshal(tt.d, &sv)
|
||||
if !reflect.DeepEqual(sv, tt.wsv) {
|
||||
t.Errorf("#%d: semver=%#v, want %#v", i, sv, tt.wsv)
|
||||
}
|
||||
if gerr := (err != nil); gerr != tt.werr {
|
||||
t.Errorf("#%d: err==%v, want errstate %t", i, err, tt.werr)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// URL wraps url.URL to marshal/unmarshal to/from JSON strings and enforce
|
||||
// that the scheme is HTTP/HTTPS only
|
||||
type URL url.URL
|
||||
|
||||
func NewURL(s string) (*URL, error) {
|
||||
uu, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bad URL: %v", err)
|
||||
}
|
||||
nu := URL(*uu)
|
||||
if err := nu.assertValidScheme(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &nu, nil
|
||||
}
|
||||
|
||||
func (u URL) String() string {
|
||||
uu := url.URL(u)
|
||||
return uu.String()
|
||||
}
|
||||
|
||||
func (u URL) assertValidScheme() error {
|
||||
switch u.Scheme {
|
||||
case "http", "https":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("bad URL scheme, must be http/https")
|
||||
}
|
||||
}
|
||||
|
||||
func (u *URL) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
nu, err := NewURL(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*u = *nu
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u URL) MarshalJSON() ([]byte, error) {
|
||||
if err := u.assertValidScheme(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(u.String())
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func mustParseURL(t *testing.T, s string) url.URL {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing URL: %v", err)
|
||||
}
|
||||
return *u
|
||||
}
|
||||
|
||||
func TestMarshalURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
u url.URL
|
||||
|
||||
w string
|
||||
}{
|
||||
{
|
||||
mustParseURL(t, "http://foo.com"),
|
||||
|
||||
`"http://foo.com"`,
|
||||
},
|
||||
{
|
||||
mustParseURL(t, "http://foo.com/huh/what?is=this"),
|
||||
|
||||
`"http://foo.com/huh/what?is=this"`,
|
||||
},
|
||||
{
|
||||
mustParseURL(t, "https://example.com/bar"),
|
||||
|
||||
`"https://example.com/bar"`,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
u := URL(tt.u)
|
||||
b, err := json.Marshal(u)
|
||||
if g := string(b); g != tt.w {
|
||||
t.Errorf("#%d: got %q, want %q", i, g, tt.w)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("#%d: err=%v, want nil", i, err)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalURLBad(t *testing.T) {
|
||||
tests := []url.URL{
|
||||
mustParseURL(t, "ftp://foo.com"),
|
||||
mustParseURL(t, "unix:///hello"),
|
||||
}
|
||||
for i, tt := range tests {
|
||||
u := URL(tt)
|
||||
b, err := json.Marshal(u)
|
||||
if b != nil {
|
||||
t.Errorf("#%d: got %v, want nil", i, b)
|
||||
}
|
||||
if err == nil {
|
||||
t.Errorf("#%d: got unexpected err=nil", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
|
||||
w URL
|
||||
}{
|
||||
{
|
||||
`"http://foo.com"`,
|
||||
|
||||
URL(mustParseURL(t, "http://foo.com")),
|
||||
},
|
||||
{
|
||||
`"http://yis.com/hello?goodbye=yes"`,
|
||||
|
||||
URL(mustParseURL(t, "http://yis.com/hello?goodbye=yes")),
|
||||
},
|
||||
{
|
||||
`"https://ohai.net"`,
|
||||
|
||||
URL(mustParseURL(t, "https://ohai.net")),
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
var g URL
|
||||
err := json.Unmarshal([]byte(tt.in), &g)
|
||||
if err != nil {
|
||||
t.Errorf("#%d: want err=nil, got %v", i, err)
|
||||
}
|
||||
if !reflect.DeepEqual(g, tt.w) {
|
||||
t.Errorf("#%d: got url=%v, want %v", i, g, tt.w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalURLBad(t *testing.T) {
|
||||
var empty = URL{}
|
||||
tests := []string{
|
||||
"badjson",
|
||||
"http://google.com",
|
||||
`"ftp://example.com"`,
|
||||
`"unix://file.net"`,
|
||||
`"not a url"`,
|
||||
}
|
||||
for i, tt := range tests {
|
||||
var g URL
|
||||
err := json.Unmarshal([]byte(tt), &g)
|
||||
if err == nil {
|
||||
t.Errorf("#%d: want err, got nil", i)
|
||||
}
|
||||
if !reflect.DeepEqual(g, empty) {
|
||||
t.Errorf("#%d: got %v, want %v", i, g, empty)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoEmptyUUID = errors.New("UUID cannot be empty")
|
||||
)
|
||||
|
||||
// UUID encodes an RFC4122-compliant UUID, marshaled to/from a string
|
||||
// TODO(jonboulle): vendor a package for this?
|
||||
// TODO(jonboulle): consider more flexibility in input string formats.
|
||||
// Right now, we only accept:
|
||||
// "6733C088-A507-4694-AABF-EDBE4FC5266F"
|
||||
// "6733C088A5074694AABFEDBE4FC5266F"
|
||||
type UUID [16]byte
|
||||
|
||||
func (u UUID) String() string {
|
||||
return fmt.Sprintf("%x-%x-%x-%x-%x", u[0:4], u[4:6], u[6:8], u[8:10], u[10:16])
|
||||
}
|
||||
|
||||
// NewUUID generates a new UUID from the given string. If the string does not
|
||||
// represent a valid UUID, nil and an error are returned.
|
||||
func NewUUID(s string) (*UUID, error) {
|
||||
s = strings.Replace(s, "-", "", -1)
|
||||
if len(s) != 32 {
|
||||
return nil, errors.New("bad UUID length != 32")
|
||||
}
|
||||
dec, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var u UUID
|
||||
for i, b := range dec {
|
||||
u[i] = b
|
||||
}
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
func (u UUID) Empty() bool {
|
||||
return reflect.DeepEqual(u, UUID{})
|
||||
}
|
||||
|
||||
func (u *UUID) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
uu, err := NewUUID(s)
|
||||
if uu.Empty() {
|
||||
return ErrNoEmptyUUID
|
||||
}
|
||||
if err == nil {
|
||||
*u = *uu
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (u UUID) MarshalJSON() ([]byte, error) {
|
||||
if u.Empty() {
|
||||
return nil, ErrNoEmptyUUID
|
||||
}
|
||||
return json.Marshal(u.String())
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package types
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNewUUID(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
ws string
|
||||
}{
|
||||
{
|
||||
"6733C088-A507-4694-AABF-EDBE4FC5266F",
|
||||
|
||||
"6733c088-a507-4694-aabf-edbe4fc5266f",
|
||||
},
|
||||
{
|
||||
"6733C088A5074694AABFEDBE4FC5266F",
|
||||
|
||||
"6733c088-a507-4694-aabf-edbe4fc5266f",
|
||||
},
|
||||
{
|
||||
"0aaf0a79-1a39-4d59-abbf-1bebca8209d2",
|
||||
|
||||
"0aaf0a79-1a39-4d59-abbf-1bebca8209d2",
|
||||
},
|
||||
{
|
||||
"0aaf0a791a394d59abbf1bebca8209d2",
|
||||
|
||||
"0aaf0a79-1a39-4d59-abbf-1bebca8209d2",
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
gu, err := NewUUID(tt.in)
|
||||
if err != nil {
|
||||
t.Errorf("#%d: err=%v, want %v", i, err, nil)
|
||||
}
|
||||
if gs := gu.String(); gs != tt.ws {
|
||||
t.Errorf("#%d: String()=%v, want %v", i, gs, tt.ws)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewUUIDBad(t *testing.T) {
|
||||
tests := []string{
|
||||
"asdf",
|
||||
"0AAF0A79-1A39-4D59-ABBF-1BEBCA8209D2ABC",
|
||||
"",
|
||||
}
|
||||
for i, tt := range tests {
|
||||
g, err := NewUUID(tt)
|
||||
if err == nil {
|
||||
t.Errorf("#%d: err=nil, want non-nil", i)
|
||||
}
|
||||
if g != nil {
|
||||
t.Errorf("#%d: err=%v, want %v", i, g, nil)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Volume encapsulates a volume which should be mounted into the filesystem
|
||||
// of all apps in a ContainerRuntimeManifest
|
||||
type Volume struct {
|
||||
Kind string `json:"kind"`
|
||||
Fulfills []ACName `json:"fulfills"`
|
||||
|
||||
// currently used only by "host"
|
||||
// TODO(jonboulle): factor out?
|
||||
Source string `json:"source,omitempty"`
|
||||
ReadOnly bool `json:"readOnly,omitempty"`
|
||||
}
|
||||
|
||||
type volume Volume
|
||||
|
||||
func (v Volume) assertValid() error {
|
||||
switch v.Kind {
|
||||
case "empty":
|
||||
return nil
|
||||
case "host":
|
||||
if v.Source == "" {
|
||||
return errors.New("source for host volume cannot be empty")
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return errors.New(`unrecognized volume type: should be one of "empty", "host"`)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Volume) UnmarshalJSON(data []byte) error {
|
||||
var vv volume
|
||||
if err := json.Unmarshal(data, &vv); err != nil {
|
||||
return err
|
||||
}
|
||||
nv := Volume(vv)
|
||||
if err := nv.assertValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
*v = nv
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v Volume) MarshalJSON() ([]byte, error) {
|
||||
if err := v.assertValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(volume(v))
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"github.com/coreos/rocket/app-container/schema/types"
|
||||
)
|
||||
|
||||
var (
|
||||
AppContainerVersion types.SemVer
|
||||
)
|
||||
|
||||
func init() {
|
||||
v, _ := types.NewSemVer("0.1.0")
|
||||
AppContainerVersion = *v
|
||||
}
|
||||
@@ -13,16 +13,6 @@ export GOPATH=${GOPATH}:${PWD}/gopath
|
||||
|
||||
eval $(go env)
|
||||
|
||||
echo "Building actool..."
|
||||
go build -o $GOBIN/actool ${REPO_PATH}/app-container/actool
|
||||
|
||||
if ! [[ -d "$(go env GOROOT)/pkg/linux_amd64" ]]; then
|
||||
echo "go linux/amd64 not bootstrapped, not building ACE validator"
|
||||
else
|
||||
echo "Building ACE validator..."
|
||||
GOARCH=amd64 GOOS=linux CGO_ENABLED=0 go build -a -ldflags '-extldflags "-static"' -o $GOBIN/ace-validator ${REPO_PATH}/app-container/ace
|
||||
fi
|
||||
|
||||
if [[ "$OSTYPE" == "linux-gnu" ]]; then
|
||||
echo "Building init (stage1)..."
|
||||
go build -o $GOBIN/init ${REPO_PATH}/stage1
|
||||
|
||||
+1
-1
@@ -7,8 +7,8 @@ import (
|
||||
"io"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/appc/spec/aci"
|
||||
"github.com/coreos/rocket/Godeps/_workspace/src/github.com/peterbourgon/diskv"
|
||||
"github.com/coreos/rocket/app-container/aci"
|
||||
pkgio "github.com/coreos/rocket/pkg/io"
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/rocket/app-container/schema/types"
|
||||
"github.com/appc/spec/schema/types"
|
||||
)
|
||||
|
||||
const tstprefix = "cas-test"
|
||||
|
||||
+1
-1
@@ -7,8 +7,8 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/appc/spec/schema/types"
|
||||
"github.com/coreos/rocket/Godeps/_workspace/src/github.com/mitchellh/ioprogress"
|
||||
"github.com/coreos/rocket/app-container/schema/types"
|
||||
)
|
||||
|
||||
func NewRemote(name string, mirrors []string) *Remote {
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/rocket/app-container/aci"
|
||||
"github.com/appc/spec/aci"
|
||||
)
|
||||
|
||||
// copy the default of git which is a two byte prefix. We will likely want to
|
||||
|
||||
+2
-2
@@ -14,9 +14,9 @@ import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/appc/spec/schema"
|
||||
"github.com/appc/spec/schema/types"
|
||||
"github.com/coreos/rocket/Godeps/_workspace/src/github.com/gorilla/mux"
|
||||
"github.com/coreos/rocket/app-container/schema"
|
||||
"github.com/coreos/rocket/app-container/schema/types"
|
||||
)
|
||||
|
||||
type metadata struct {
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ package path
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/coreos/rocket/app-container/schema/types"
|
||||
"github.com/appc/spec/schema/types"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/coreos/rocket/app-container/discovery"
|
||||
"github.com/appc/spec/discovery"
|
||||
"github.com/coreos/rocket/cas"
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/rocket/app-container/schema/types"
|
||||
"github.com/appc/spec/schema/types"
|
||||
"github.com/coreos/rocket/cas"
|
||||
"github.com/coreos/rocket/stage0"
|
||||
)
|
||||
|
||||
+3
-3
@@ -24,10 +24,10 @@ import (
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/appc/spec/aci"
|
||||
"github.com/appc/spec/schema"
|
||||
"github.com/appc/spec/schema/types"
|
||||
"github.com/coreos/rocket/Godeps/_workspace/src/code.google.com/p/go-uuid/uuid"
|
||||
"github.com/coreos/rocket/app-container/aci"
|
||||
"github.com/coreos/rocket/app-container/schema"
|
||||
"github.com/coreos/rocket/app-container/schema/types"
|
||||
"github.com/coreos/rocket/cas"
|
||||
rktpath "github.com/coreos/rocket/path"
|
||||
ptar "github.com/coreos/rocket/pkg/tar"
|
||||
|
||||
+2
-2
@@ -12,9 +12,9 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/appc/spec/schema"
|
||||
"github.com/appc/spec/schema/types"
|
||||
"github.com/coreos/rocket/Godeps/_workspace/src/github.com/coreos/go-systemd/unit"
|
||||
"github.com/coreos/rocket/app-container/schema"
|
||||
"github.com/coreos/rocket/app-container/schema/types"
|
||||
rktpath "github.com/coreos/rocket/path"
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ package main
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/coreos/rocket/app-container/schema/types"
|
||||
"github.com/appc/spec/schema/types"
|
||||
"github.com/coreos/rocket/path"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
# Run tests for one package
|
||||
#
|
||||
# PKG=./app-container/discovery ./test
|
||||
# PKG=./cas ./test
|
||||
# PKG=cas ./test
|
||||
|
||||
# Invoke ./cover for HTML output
|
||||
@@ -14,8 +14,8 @@ COVER=${COVER:-"-cover"}
|
||||
|
||||
source ./build
|
||||
|
||||
TESTABLE_AND_FORMATTABLE="app-container/discovery app-container/schema/types cas pkg/tar"
|
||||
FORMATTABLE="$TESTABLE_AND_FORMATTABLE app-container/ace app-container/aci app-container/actool app-container/schema metadatasvc path pkg/io pkg/proc pkg/tarheader rkt stage0/run.go stage1 version"
|
||||
TESTABLE_AND_FORMATTABLE="cas pkg/tar"
|
||||
FORMATTABLE="$TESTABLE_AND_FORMATTABLE metadatasvc path pkg/io pkg/proc pkg/tarheader rkt stage0/run.go stage1 version"
|
||||
|
||||
# user has not provided PKG override
|
||||
if [ -z "$PKG" ]; then
|
||||
@@ -40,12 +40,6 @@ TEST=${split[@]/#/${REPO_PATH}/}
|
||||
echo "Running tests..."
|
||||
go test -timeout 60s ${COVER} $@ ${TEST} --race
|
||||
|
||||
echo "Validating app manifest..."
|
||||
bin/actool validate app-container/examples/app.json
|
||||
|
||||
echo "Validating container runtime manifest..."
|
||||
bin/actool validate app-container/examples/container.json
|
||||
|
||||
echo "Checking gofmt..."
|
||||
fmtRes=$(gofmt -l $FMT)
|
||||
if [ -n "${fmtRes}" ]; then
|
||||
|
||||
Reference in New Issue
Block a user