*: 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:
Jonathan Boulle
2014-12-09 12:27:35 -08:00
parent f3b103a32b
commit a1d7eb8e9c
66 changed files with 29 additions and 4466 deletions
+1 -1
View File
@@ -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
+7 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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|"&lt;uint&gt;" |"4096" |
|memory/limit |string|"&lt;bytes&gt;" |"1G", "5T", "4K"|
|blockIO/readBandwidth |string|"&lt;path to file&gt; &lt;bytes&gt;"|"/tmp 1K" |
|blockIO/writeBandwidth |string|"&lt;path to file&gt; &lt;bytes&gt;"|"/tmp 1K" |
|networkIO/readBandwidth |string|"&lt;device name&gt; &lt;bytes&gt;" |"eth0 100M" |
|networkIO/writeBandwidth |string|"&lt;device name&gt; &lt;bytes&gt;" |"eth0 100M" |
|privateNetwork |string|"&lt;true&#124;false&gt;" |"true" |
|capabilities/boundingSet |string|"&lt;cap&gt; &lt;cap&gt; ..." |"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=&lt;base64 encoded signature&gt; and uid=&lt;uid of the container that generated the signature&gt;. 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
-65
View File
@@ -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
}
]
}
}
-36
View File
@@ -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
-533
View File
@@ -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
}
}
}
}
-102
View File
@@ -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
}
-44
View File
@@ -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
}
-156
View File
@@ -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
}
-84
View File
@@ -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()
}
-102
View File
@@ -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"))
}
-164
View File
@@ -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
}
-51
View File
@@ -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
}
-125
View File
@@ -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()
}
-195
View File
@@ -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
}
-19
View File
@@ -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
}
-112
View File
@@ -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
}
-75
View File
@@ -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])
}
}
}
-39
View File
@@ -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
}
-15
View File
@@ -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>
-80
View File
@@ -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
}
-106
View File
@@ -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"
}
}
-53
View File
@@ -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"
}
}
-64
View File
@@ -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
}
-77
View File
@@ -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"`
}
-16
View File
@@ -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.
*/
-28
View File
@@ -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))
}
-53
View File
@@ -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
}
-45
View File
@@ -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)
}
}
}
-66
View File
@@ -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())
}
-42
View File
@@ -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)
}
}
}
-51
View File
@@ -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
}
-52
View File
@@ -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
}
-46
View File
@@ -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())
}
-66
View File
@@ -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)
}
}
}
-29
View File
@@ -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
}
-79
View File
@@ -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
}
-82
View File
@@ -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)
}
}
}
-6
View File
@@ -1,6 +0,0 @@
package types
type Isolator struct {
Name ACName `json:"name"`
Val string `json:"val"`
}
-59
View File
@@ -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
}
-7
View File
@@ -1,7 +0,0 @@
package types
type MountPoint struct {
Name ACName `json:"name"`
Path string `json:"path"`
ReadOnly bool `json:"readOnly"`
}
-8
View File
@@ -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"`
}
-62
View File
@@ -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())
}
-115
View File
@@ -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)
}
}
}
-57
View File
@@ -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())
}
-123
View File
@@ -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)
}
}
}
-70
View File
@@ -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())
}
-59
View File
@@ -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)
}
}
}
-54
View File
@@ -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))
}
-14
View File
@@ -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
}
-10
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
)
+3 -9
View File
@@ -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