From cb6b3f1abebf55ae0c2b1d88c460e5f5145d0e32 Mon Sep 17 00:00:00 2001 From: Evan Anderson Date: Thu, 15 Feb 2018 20:07:42 +0000 Subject: [PATCH 01/12] Define software personas for Elafros. --- docs/product/personas.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 docs/product/personas.md diff --git a/docs/product/personas.md b/docs/product/personas.md new file mode 100644 index 000000000000..7fe34bb2bca0 --- /dev/null +++ b/docs/product/personas.md @@ -0,0 +1,40 @@ +# Elafros Personas + +When discussing user actions, it is often helpful to [define specific +user roles](http://www.agilemodeling.com/artifacts/personas.htm) who +might want to do the action. + + +## Elafros Compute + +### Developer Personas + +These are generally software engineres looking to build and run a +stateless software application, without concern about the underlying +infrastructure. + +* Hobbyist +* "Backend" SWE +* "Full stack" SWE +* SRE + +### Operator Personas + +* Hobbyist / Contributor +* Cluster administrator +* Security Engineer / Auditor +* Capacity Planner + + +## Elafros Build + +* Developer + - Start a build + - Read build logs +* Language operator + - Create a build path/build pack + +## Elafros Events + +* Event consumer (developer) +* Event producer \ No newline at end of file From 0fc38578263667e0d481473269bad585ebbe02a2 Mon Sep 17 00:00:00 2001 From: Evan Anderson Date: Tue, 6 Mar 2018 06:41:29 +0000 Subject: [PATCH 02/12] Initial copy/commit of Elafros API design doc, chunked into sections. Still TODO: - Appendices A-E. - Resolve diffs between spec.md and concrete API definition. - Define process for updating the API specification. - Additional API specifications covering: * Logging / monitoring / observability contract * Runtime container contract / environment --- docs/spec/errors.md | 304 +++++++++++ docs/spec/motivation.md | 21 + docs/spec/normative_examples.md | 923 ++++++++++++++++++++++++++++++++ docs/spec/overview.md | 81 +++ docs/spec/spec.md | 249 +++++++++ 5 files changed, 1578 insertions(+) create mode 100644 docs/spec/errors.md create mode 100644 docs/spec/motivation.md create mode 100644 docs/spec/normative_examples.md create mode 100644 docs/spec/overview.md create mode 100644 docs/spec/spec.md diff --git a/docs/spec/errors.md b/docs/spec/errors.md new file mode 100644 index 000000000000..62c3fdfb85d5 --- /dev/null +++ b/docs/spec/errors.md @@ -0,0 +1,304 @@ +# Error Conditions and Reporting + +The standard kubernetes API pattern for reporting configuration errors +and current state of the system is to use the status section to report +the state of the system observed by the controller. There are two +mechanisms commonly used in status: + +* conditions represent true/false statements about the current state + of the resource. + +* other fields may provide status on the most-recently retrieved state + of the world as it relates to the resource (example: number of + replicas or traffic assignments). + +Both of these mechanisms will often include additional data from the +controller such as `observedGeneration` (to determine whether the +controller has seen the latest updates to the spec). Below are some +example error scenarios (either due to user or system error), along +with how the status would be presented to CLI and UI tools via the +API: + +## Revision failed to become Ready + +If the latest Revision fails to become "Ready" within some reasonable +timeframe (for whatever reason), the Configuration should signal this +with the `LatestRevisionReady` status, copying the reason and message +from the Ready condition on the Revision. + +```yaml +... +status: + latestReadyRevisionName: abc + latestCreatedRevisionName: bcd # Hasn't become became "Ready" + conditions: + - type: LatestRevisionReady + status: False + reason: ContainerMissing + message: "Unable to start because container is missing and build failed." +``` + + +## Build failed + +If the Build steps failed while creating a Revision, this would be +detectable by examining the Failed condition on the Build or the +BuildFailed condition on the Revision (which copies the value from the +build referenced by `spec.buildName`). In addition, the Build resource +(but not the Revision) should have a status field to link to the log +output of the build. + +```http +GET /apis/build.dev/v1alpha1/namespaces/default/builds/build-1acub3 +``` +```yaml +... +status: + # Link to log stream; could be ELK or Stackdriver, for example + buildLogsLink: "http://logging.infra.mycompany.com/...?filter=..." + conditions: + - type: Complete + status: True + - type: BuildFailed + status: True + reason: BuildStepFailed # could also be SourceMissing, etc + # reason is for machine consumption, message is for human consumption + message: "Step XYZ failed with error message: $LASTLOGLINE" +``` + + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/revisions/abc +``` +```yaml +... +status: + conditions: + - type: Ready + status: False + reason: ContainerMissing + message: "Unable to start because container is missing and build failed." + - type: BuildFailed + status: True + reason: BuildStepFailed + # reason is for machine consumption, message is for human consumption + message: "Step XYZ failed with error message: $LASTLOGLINE" +``` + + +## Revision not found by Route + +If a Revision is referenced in the Route's `spec.rollout.traffic`, the +corresponding entry in the `status.traffic` list will be set to the name +"Not found", and the `TrafficDropped` condition would be marked as True, +with a reason of `RevisionMissing`. + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/routes/abc +``` +```yaml +... +status: + traffic: + - revisionName: abc + name: current + percent: 100 + - revisionName: "Not found" + name: next + percent: 0 + conditions: + - type: RolloutInProgress + status: False + - type: TrafficDropped + status: True + reason: RevisionMissing + # reason is for machine consumption, message is for human consumption + message: "Revision 'qyzz' referenced in rollout.traffic not found" +``` + + +## Configuration not found by Route + +If a Route references the `latestReadyRevisionName` of a Configuration +and the Configuration cannot be found, the corresponding entry in +`status.traffic` list will be set to the name "Not found", and the +`TrafficDropped` condition would be marked as True with a reason of +`ConfigurationMissing`. + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/routes/abc +``` +```yaml +... +status: + traffic: + - revisionName: "Not found" + percent: 100 + conditions: + - type: RolloutInProgress + status: False + - type: TrafficDropped + status: True + reason: ConfigurationMissing + # reason is for machine consumption, message is for human consumption + message: "Revision 'my-service' referenced in rollout.traffic not found" +``` + + +## Latest Revision of a Configuration deleted + +If the most recent (or most recently ready) Revision is deleted, the +Configuration will clear the `latestReadyRevisionName`. If the +Configuration is referenced by a Route, then the Route will set the +`TrafficDropped` condition with reason `RevisionMissing`, as above. + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/configurations/my-service +``` +```yaml +... +metadata: + generation: 1234 # only updated when spec changes + ... +spec: + ... +status: + latestCreatedRevision: abc + observedGeneration: 1234 +``` + + +## Resource exhausted while creating a revision + +Since a Revision is only metadata, the Revision will be created, but +may have a condition indicating the underlying failure, possibly +including the associated underlying resource. In a multitenant +environment, the customer may not have have access or visibility into +the underlying resources in the hosting environment. + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/revisions/abc +``` +```yaml +... +status: + conditions: + - type: Ready + status: False + reason: NoDeployment + message: "The controller could not create a deployment named ela-abc-e13ac." +``` + + +## Deployment progressing slowly/stuck + +See +[the kubernetes documentation for how this is handled for Deployments](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#failed-deployment). For +Revisions, we would start by assuming a single timeout for deployment +(rather than configurable), and report that the Revision was not +Ready, with a reason `ProgressDeadlineExceeded`. Note that we would only +report `ProgressDeadlineExceeded` if we could not determine another +reason (such as quota failures, missing build, or container execution +failures). + +Kubernetes controllers will continue attempting to make progress +(possibly at a less-aggressive rate) when they encounter a case where +the desired status cannot match the actual status, so if the +underlying deployment is slow, it may eventually finish after +reporting `ProgressDeadlineExceeded`. + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/revisions/abc +``` +```yaml +... +status: + conditions: + - type: Ready + status: False + reason: ProgressDeadlineExceeded + message: "Unable to create pods for more than 120 seconds." +``` + + +## Traffic shift progressing slowly/stuck + +Similar to deployment slowness, if the transfer of traffic (either via +gradual rollout or knife-switch) takes longer than a certain timeout +to complete/update, the `RolloutInProgress` condition would remain at +true, but the reason would be set to `ProgressDeadlineExceeded`. + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/routes/abc +``` +```yaml +... +status: + traffic: + - revisionName: abc + percent: 75 + - revisionName: def + percent: 25 + conditions: + - type: RolloutInProgress + status: True + reason: ProgressDeadlineExceeded + # reason is for machine consumption, message is for human consumption + message: "Unable to update traffic split for more than 120 seconds." +``` + + +## Container image not present in repository + +We expect that Revisions may be created while a Build is still +creating the container image or uploading it to the repository. If the +build is being performed by a CRD in the cluster, the spec.buildName +attribute will be set (and see the "Build failed" example). In other +cases when the build is not supplied, the container image referenced +may not be present in the registry (either because of a typo or +because it was deleted). In this case, the Ready condition will be set +to False with a reason of ContainerMissing. This condition could be +corrected if the image becomes available at a later time. We may also +make a defensive copy of the container image (e.g. in Riptide) to +avoid this error due to deleted source container. + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/revisions/abc +``` +```yaml +... +status: + conditions: + - type: Ready + status: False + reason: ContainerMissing + message: "Unable to fetch image 'gcr.io/...': " +``` + + +## Container image fails at startup on Revision + +Particularly for development cases with interpreted languages like +Node or Python, syntax errors or the like might only be caught at +container startup time. For this reason, implementations may choose to +start a single copy of the container on deployment, before making the +container Ready. If the initial container fails to start, the `Ready` +condition will be set to False and the reason will be set to +`ExitCode:%d` with the exit code of the application, and the last line +of output in the message. Additionally, the Revision will include a +`logsUrl` which provides the address of an endpoint which can be used to +fetch the logs for the failed process. + +```http +GET /apis/elafros.dev/v1alpha1/namespaces/default/revisions/abc +``` +```yaml +... +status: + logUrl: "http://logging.infra.mycompany.com/...?filter=revision=abc&..." + conditions: + - type: Ready + status: False + reason: ExitCode:127 + message: "Container failed with: SyntaxError: Unexpected identifier" +``` diff --git a/docs/spec/motivation.md b/docs/spec/motivation.md new file mode 100644 index 000000000000..d683600731bc --- /dev/null +++ b/docs/spec/motivation.md @@ -0,0 +1,21 @@ +The goal of the Elafros project is to provide a common toolkit and API +framework for serverless workloads. + +We define serverless workloads as computing workloads which are: + +* Stateless. +* Amenable to the process scale-out model. +* Primarily driven by application level (L7 -- HTTP, for example) + request traffic. + +While Kubernetes provides basic primitives like Deployment, Service, +and Ingress in support of this model, our experience suggests that a +more compact and richer opinionated model has substantial benefit for +developers. In particular, by standardizing on higher-level primitives +which perform substantial amounts of automation of common primitives, +it should be possible to build consistent toolkits which provide a +richer experience than via yaml files with `kubectl`. + +The Elafos APIs are composed of three facets; this document primarily +concerns itself with the compute API; the other two APIs are Build and +Eventing. diff --git a/docs/spec/normative_examples.md b/docs/spec/normative_examples.md new file mode 100644 index 000000000000..b9c21b769851 --- /dev/null +++ b/docs/spec/normative_examples.md @@ -0,0 +1,923 @@ +# Sample API Usage + +Following are several normative sample scenarios utilizing the Elafros +API. These scenarios are arranged to provide a flavor of the API and +building from the smallest, most frequent operations. + +Examples in this section illustrate: + +* Creating a first route to deploy a first revision from a pre-built + container + +* Automatic and manual rollout options for deploying subsequent + revisions + +* Creating a revision from source + +* Creating a function + +Additional common scenarios, including rollback and listing resources, +are shown in [Appendix A](additional_examples.md). More sophisticated +rollout scenarios that the API may support, including n-way traffic +splitting, are shown in [Appendix B](complex_examples.md). + +Note that these API operations are identical for both app and function +based services. (to see the full resource definitions, see the +[Resource YAML Definitions](spec.md)). + +CLI samples are for illustrative purposes, and not intended to +represent final CLI design. + +## 1) Automatic rollout of a new Revision to existing Service - pre-built container + +**_Scenario_**: User deploys a new revision to an existing service +with a new container image, rolling out automatically to 100% + +``` +$ elafros deploy --service my-service + Deploying app to service [my-service]: +✓ Starting +✓ Promoting + Done. + Deployed to https://my-service.default.mydomain.com +``` + + +**Steps**: + +* Update the Configuration with the config change + +**Results:** + +* A new Revision is created, and automatically rolled out to 100% once + ready + +After the initial Route and Configuration have been created (which is +shown in the [second example]()), the typical +interaction is to update the revision configuration, resulting in the +creation of a new revision, which will be automatically rolled out by +the route. Revision configuration updates can be handled as either a +PUT or PATCH operation: + +* Optimistic concurrency controls for PUT operations in a + read/modify/write routine work as expected in kubernetes. + +* PATCH semantics should work as expected in kubernetes, but may have + some limitations imposed by CRDs at the moment. + +In this and following examples PATCH is used. Revisions can be built +from source, which results in a container image, or by directly +supplying a pre-built container, which this first scenario +illustrates. The example demonstrates the PATCH issued by the client, +followed by several GET calls to illustrate each step in the +reconciliation process as the system materializes the new revision, +and begins shifting traffic from the old revision to the new revision. + +The client PATCHes the configuration's template revision with just the +new container image, inheriting previous configuration from the +configuration: + +```http +PATCH /apis/elafros.dev/namespaces/default/configurations/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Configuration +metadata: + name: my-service # by convention, same name as the service +spec: + revisionTemplate: # template for building Revision + spec: + container: + image: gcr.io/... # new image +``` + +The update to the configuration triggers a new revision being created, +and the configuration is updated to reflect the new revision: + +```http +GET /apis/elafros.dev/namespaces/default/configurations/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Configuration +metadata: + name: my-service + generation: 1235 + ... + +spec: + ... # same as before, except new container.image +status: + latestReadyRevisionName: abc + latestCreatedRevisionName: def # new revision created, but not ready yet + observedGeneration: 1235 +``` + +The newly created revision has the same config as the previous +revision, but different code. Note the generation label reflects the +new generation of the configuration (1235), indicating the provenance +of the revision: + +```http +GET /apis/elafros.dev/namespaces/default/revisions/def +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Revision +metadata: + name: def + labels: + elafros.dev/configuration: my-service + elafros.dev/configurationGeneration: 1235 + ... +spec: + container: # k8s core.v1.Container + image: gcr.io/... # new container + # same config as previous revision + env: + - name: FOO + value: bar + - name: HELLO + value: blurg + ... +status: + conditions: + - type: Ready + status: True +``` + +When the new revision is Ready, i.e. underlying resources are +materialized and ready to serve, the configuration updates its +`status.latestReadyRevisionName` status to reflect the new +revision. The route, which is configured to automatically rollout new +revisions from the configuration, watches the configuration and is +notified of the `latestReadyRevisionName`, and begins migrating traffic +to it. During reconciliation, traffic may be routed to both existing +revision `abc` and new revision `def`: + +```http +GET /apis/elafros.dev/namespaces/default/routes/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service + ... +spec: + rollout: + traffic: + - configurationName: my-service + percent: 100 + +status: + # domain: + # oss: my-service.namespace.mydomain.com + domain: my-service.namespace.mydomain.com + # percentages add to 100 + traffic: # in status, all configurationName refs are dereferenced + - revisionName: abc + percent: 75 + - revisionName: def + percent: 25 + conditions: + - type: RolloutInProgress + status: True +``` + +And once reconciled, revision def serves 100% of the traffic : + +```http +GET /apis/elafros.dev/namespaces/default/routes/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service + ... +spec: + rollout: + traffic: + - configurationName: my-service + percent: 100 +status: + domain: my-service.default.mydomain.com + traffic: + - revisionName: def + percent: 100 + conditions: + - type: RolloutInProgress + status: False + ... +``` + + +## 2) Creating Route and deploying first Revision - pre-built container + +**Scenario**: User creates a new Route and deploys their first + Revision based on a pre-built container + +``` +$ elafros deploy --service my-service --region us-central1 +✓ Creating service [my-service] in region [us-central1] + Deploying app to service [my-service]: +✓ Uploading [=================] +✓ Starting +✓ Promoting + Done. + Deployed to https://my-service.default.mydomain.com +``` + +**Steps**: + +* Create a new Configuration and a Route that references a that + configuration. + +**Results**: + +* A new Configuration is created, and generates a new Revision based + on the configuration + +* A new Route is created, referencing the configuration + +* The route begins serving traffic to the revision that was created by + the configuration + +The previous example assumed an existing Route and Configuration to +illustrate the common scenario of updating the configuration to deploy +a new revision to the service. + +In this getting started example, deploying a first Revision is +accomplished by creating a new Configuration (which will generate a +new Revision) and creating a new Route referring to that +configuration. Note that these two steps can occur in either order, or +in parallel. + +A Route can either refer directly to a Revision, or to the latest +ready revision of a Configuration, as this example illustrates. This +is the most straightforward scenario that many Elafros customers are +expected to use, and is consistent with the experience of deploying +code that is rolled out immediately. + +The example shows the POST calls issued by the client, followed by +several GET calls to illustrate each step in the reconciliation +process as the system materializes and begins routing traffic to the +revision. + +The client creates the route and configuration, which by convention +share the same name: + +```http +POST /apis/elafros.dev/namespaces/default/routes +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service +spec: + rollout: + traffic: + - configurationName: my-service # named reference to Configuration + percent: 100 # automatically activate new Revisions from the configuration +``` + +```http +POST /apis/elafros.dev/namespaces/default/configurations +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Configuration +metadata: + name: my-service # By convention (not req'd), same name as the service. + # This will also be set as the "elafros.dev/configuration" + # label on the created Revision. +spec: + revisionTemplate: # template for building Revision + metadata: ... + spec: + container: # k8s core.v1.Container + image: gcr.io/... + env: + - name: FOO + value: bar + - name: HELLO + value: world + ... +``` + +Upon the creation of the configuration, the system will create a new +Revision, generating its name, and applying the spec and metadata from +the configuration, as well as new metadata labels: + +```http +GET /apis/elafros.dev/namespaces/default/revisions/abc +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Revision +metadata: + name: abc # generated name + labels: + # name and generation of the configuration that created the revision + elafros.dev/configuration: my-service + elafros.dev/configurationGeneration: 1234 + ... # uid, resourceVersion, creationTimestamp, generation, selfLink, etc +spec: + ... # spec from the configuration +status: + conditions: + - type: Ready + status: False + message: "Starting Instances" +``` + +Immediately after the revision is created, i.e. before underlying +resources have been fully materialized, the configuration is updated +with latestCreatedRevisionName: + +```http +GET /apis/elafros.dev/namespaces/default/configurations/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Configuration +metadata: + name: my-service + generation: 1234 + ... # uid, resourceVersion, creationTimestamp, selfLink, etc +spec: + ... # same as before +status: + # latest created revision, may not have materialized yet + latestCreatedRevisionName: abc + observedGeneration: 1234 +``` + +The configuration watches the revision, and when the revision is +updated as Ready (to serve), the latestReadyRevisionName is updated: + +```http +GET /apis/elafros.dev/namespaces/default/configurations/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Configuration +metadata: + name: my-service + generation: 1234 + ... +spec: + ... # same as before +status: + # the latest created and ready to serve. Watched by service + latestReadyRevisionName: abc + # latest created revision + latestCreatedRevisionName: abc + observedGeneration: 1234 +``` + +The route, which watches the configuration `my-service`, observes the +change to `latestReadyRevisionName` and begins routing traffic to the +new revision `abc`, addressable as +`my-service.default.mydomain.com`. Once reconciled: + +```http +GET /apis/elafros.dev/namespaces/default/routes/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service + generation: 2145 + ... +spec: + rollout: + traffic: + - configurationName: my-service + percent: 100 + +status: + domain: my-service.default.mydomain.com + + traffic: # in status, all configurationName refs are dereferenced to latest revision + - revisionName: abc # latestReadyRevisionName from configurationName in spec + percent: 100 + + conditions: + - type: RolloutInProgress + status: False + + observedGeneration: 2145 +``` + + +## 3) Manual rollout of a new Revision - config change only + +**_Scenario_**: User updates configuration with new configuration (env + var change) to an existing service, tests the revision, then + proceeds with a manually controlled rollout to 100% + +``` +$ elafros rollout strategy manual + +$ elafros deploy --service my-service --env HELLO="blurg" +[...] + +$ elafros revisions list --service my-service +Name Traffic Id Date Deployer Git sha +next 0% v3 2018-01-19 12:16 user1 64d79ce +current 100% v2 2018-01-18 20:34 user1 a6f92d1 + v1 2018-01-17 10:32 user1 33643fc + +$ elafros rollout next percent 5 +[...] +$ elafros rollout next percent 50 +[...] +$ elafros rollout finish +[...] + +$ elafros revisions list --service my-service +Name Traffic Id Date Deployer Git sha +current,next 100% v3 2018-01-19 12:16 user1 64d79ce + v2 2018-01-18 20:34 user1 a6f92d1 + v1 2018-01-17 10:32 user1 33643fc +``` + +**Steps**: + +* Update the Route to pin the current revision + +* Update the Configuration with the new configuration (env var) + +* Update the Route to address the new Revision + +* After testing the new revision through the named subdomain, proceed + with the rollout, incrementally increasing traffic to 100% + +**Results:** + +* The system creates the new revision from the configuration, + addressable at next.my-service... (by convention), but traffic is + not routed to it until the percentage is manually ramped up. Upon + completing the rollout, the next revision is now the current + revision + +In the previous examples, the route referenced a Configuration for +automatic rollouts of new Revisions. While this pattern is useful for +many scenarios such as functions-as-a-service and simple development +flows, the Route can also reference Revisions directly to "pin" +traffic to specific revisions, which is suitable for manually +controlling rollouts, i.e. testing a new revision prior to serving +traffic. (Note: see [Appendix B](complex_examples.md) for a +semi-automatic variation of manual rollouts). + +The client updates the route to pin the current revision: + +```http +PATCH /apis/elafros.dev/namespaces/default/routes/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service +spec: + rollout: + traffic: + - revisionName: def # pin a specific revision, i.e. the current one + percent: 100 +``` + +As in the previous example, the configuration is updated to trigger +the creation of a new revision, in this case updating the container +image but keeping the same config: + +```http +PATCH /apis/elafros.dev/namespaces/default/configurations/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Configuration +metadata: + name: my-service +spec: + revisionTemplate: + spec: + container: + env: # k8s-style strategic merge patch, updating a single list value + - name: HELLO + value: blurg # changed value +``` + +A new revision `ghi` is created that has the same code as the previous +revision `def`, but different config: + +```http +GET /apis/elafros.dev/namespaces/default/revisions/ghi +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Revision +metadata: + name: ghi + ... +spec: + container: + image: gcr.io/... # same container as previous revision abc + env: + - name: FOO + value: bar + - name: HELLO + value: blurg # changed value + ... +status: + conditions: + - type: Ready + status: True +``` + +Even when ready, the new revision does not automatically start serving +traffic, as the route was pinned to revision `def`. + +Update the route to make the existing revision serving traffic +addressable through subdomain `current`, and referencing the new +revision at 0% traffic but making it addressable through subdomain +`next`: + +```http +PATCH /apis/elafros.dev/namespaces/default/routes/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service +spec: + rollout: + traffic: + - revisionName: def + name: current # addressable as current.my-service.default.mydomain.com + percent: 100 + - revisionName: ghi + name: next # addressable as next.my-service.default.mydomain.com + percent: 0 # no traffic yet +``` + +In this state, the route makes both revisions addressable with +subdomains `current` and `next` (once the revision `ghi` has a status of +Ready), but traffic has not shifted to next yet. Also note that while +the names current/next have semantic meaning, they are convention +only; blue/green, or any other subdomain names could be configured. + +```http +GET /apis/elafros.dev/namespaces/default/routes/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service + ... +spec: + ... # unchanged +status: + domain: my-service.default.mydomain.com + traffic: + - revisionName: def + name: current # addressable as current.my-service.default.mydomain.com + percent: 100 + - revisionName: ghi + name: next # addressable as next.my-service.default.mydomain.com + percent: 0 + conditions: + - type: RolloutInProgress + status: False # Underlying routes as set in config. + ... +``` + +After testing the new revision at +`next.my-service.default.mydomain.com`, it can be rolled out to 100% +(either directly, or through several increments, with the split +totaling 100%): + +```http +PATCH /apis/elafros.dev/namespaces/default/routes/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service +spec: + rollout: + Traffic: # percentages must total 100% + - revisionName: def + name: current + percent: 0 + - revisionName: ghi + name: next + percent: 100 # migrate traffic fully to the next revision +``` + +After reconciliation, all traffic has been shifted to the new version: + +```http +GET /apis/elafros.dev/namespaces/default/routes/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service + ... +spec: + ... # unchanged +status: + domain: my-service.default.mydomain.com + traffic: + - revisionName: def + name: current + percent: 0 + - revisionName: ghi + name: next + percent: 100 + conditions: + - type: RolloutInProgress + status: False + - type: RolloutAtTargetPercentage + status: True + ... +``` + +By convention, the final step when completing the rollout is to update +`current` to reflect the new revision. `next` can either be removed, or +left addressing the same revision as current so that +`next.my-service.default.mydomain.com` is always addressable. + +```http +PATCH /apis/elafros.dev/namespaces/default/routes/my-service +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service +spec: + rollout: + traffic: + - revisionName: ghi # update for the next rollout, current = next + name: current + percent: 100 + - revisionName: ghi # optional: leave next as also referring to ghi + name: next + percent: 0 +``` + + +## 4) Deploy a Revision from source + +**Scenario**: User deploys a revision to an existing service from + source rather than a pre-built container + +``` +$ elafros deploy --service my-service + Deploying app to service [my-service]: +✓ Uploading [=================] +✓ Detected [node-8-9-4] runtime +✓ Building +✓ Starting +✓ Promoting + Done. + Deployed to https://my-service.default.mydomain.com +``` + +**Steps**: + +* Create/Update a Configuration, inlining build details. + +**Results**: + +* The Configuration is created/updated, which generates a container + build and a new revision based on the template, and can be rolled + out per earlier examples + +Previous examples demonstrated configurations created with pre-built +containers. Revisions can also be created by providing build +information to the configuration, which results in a container image +built by the system. The build information is supplied by inlining the +BuildSpec of a Build resource in the Configuration. This describes: + +* **What** to build (`build.source`): Source can be provided as an + archive, manifest file, or repository. + +* **How** to build (`build.template`): a + [BuildTemplate](https://github.com/elafros/build) is referenced, + which describes how to build the container via a builder with + arguments to the build process. + +* **Where** to publish (`build.template.arguments`): Image registry + url and other information specific to this build invocation. + +The client creates the configuration inlining a build spec for an +archive based source build, and referencing a nodejs build template: + +```http +POST /apis/elafros.dev/namespaces/default/configurations +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Configuration +metadata: + name: my-service +spec: + build: # build.dev/v1alpha1.BuildTemplateSpec + source: + # oneof archive|manifest|repository: + archive: + url: https://... + sha: ... + template: # defines build template + name: nodejs_8_9_4 # builder name + namespace: build-templates + arguments: + - name: _IMAGE + value: gcr.io/... # destination for image + + revisionTemplate: # template for building Revision + metadata: ... + spec: + container: # k8s core.v1.Container + image: gcr.io/... # Promise of a future build. Same as supplied in + # build.template.arguments[_IMAGE] + env: # Updated environment variables to go live with new source. + - name: FOO + value: bar + - name: HELLO + value: world +``` + +Note the `revisionTemplate.spec.container.image` above is supplied +with the destination of the build. This enables one-step changes to +both config and source code. If the build step were responsible for +updating the `revisionTemplate.spec.container.image` at the completion +of the build, an update to both source and config could result in the +creation of two Revisions, one with the config change, and the other +with the new code deployment. It is expected that Revision will wait +for the `revisionTemplate.spec.container.image` to be live before +marking the Revision as "ready". + +Upon creating/updating the configuration's build field, the system +creates a new revision. The configuration controller will initiate a +build, populating the revision’s buildName with a reference to the +underlying Build resource. Via status updates which the revision +controller observes through the build reference, the high-level state +of the build is mirrored into conditions in the Revision’s status: + +```http +GET /apis/elafros.dev/namespaces/default/revisions/abc +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Revision +metadata: + name: abc + labels: + elafros.dev/configuration: my-service + elafros.dev/configurationGeneration: 1234 + ... +spec: + # name of the build.dev/v1alpha1.Build, if built from source. + # Set by Configuration. + buildName: ... + + # spec from the configuration, with container.image containing the + # newly built container + container: # k8s core.v1.Container + image: gcr.io/... + env: + - name: FOO + value: bar + - name: HELLO + value: world +status: + # This is a copy of metadata from the container image or grafeas, indicating + # the provenance of the revision, annotated on the container + imageSource: + archive|manifest|repository: ... + context: ... + conditions: + - type: Ready + status: True + - type: BuildComplete + status: True + # other conditions indicating build failure details, if applicable +``` + +Rollout operations in the route are identical to the pre-built +container examples. + +Also analogous is updating the configuration to create a new +revision - in this case, updated source would be provided to the +configuration's inlined build spec, which would initiate a new +container build, and the creation of a new revision. + + +## 5) Deploy a Function + +**Scenario**: User deploys a new function revision to an existing service + +``` +$ elafros deploy --function index --service my-function + Deploying function to service [my-function]: +✓ Uploading [=================] +✓ Detected [node-8-9-4] runtime +✓ Building +✓ Starting +✓ Promoting + Done. + Deployed to https://my-function.default.mydomain.com +``` + + +**Steps**: + +* Create/Update a Configuration, additionally specifying function details. + +**Results**: + +* The Configuration is created/updated, which generates a new revision + based on the template build and spec which can be rolled out per + previous examples + +Previous examples illustrated creating and deploying revisions in the +context of apps. Functions are created and deployed in the same +manner (in particular, as containers which respond to HTTP). In the +build phase of the deployment, additional function metadata may be +taken into account in order to wrap the supplied code in a functions +framework. + +Functions are configured with a language-specific entryPoint. The +entryPoint may be provided as an argument to the build template, if +language-native autodetection is insufficient. By convention, a type +metadata label may also be added that designates revisions as a +function, supporting listing revisions by type; there is no change to +the system behavior based on type. + +Note that a function may be connected to one or more event sources via +Bindings in the Eventing API; the binding of events to functions is +not a core function of the compute API. + +Creating the configuration with build and function metadata: + +```http +POST /apis/elafros.dev/namespaces/default/configurations +``` +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Configuration +metadata: + name: my-function +spec: + build: # build.dev/v1alpha1.BuildTemplateSpec + source: + # oneof archive|manifest|repository: + archive: + url: https://... + sha: ... + template: # defines build template + name: go_1_9_fn # function builder + namespace: build-templates + arguments: + - name: _IMAGE + value: gcr.io/... # destination for image + - name: _ENTRY_POINT + value: index # language dependent, function-only entrypoint + + revisionTemplate: # template for building Revision + metadata: + labels: + # One-of "function" or "app", convention for CLI/UI clients to list/select + elafros.dev/type: "function" + spec: + container: # k8s core.v1.Container + image: gcr.io/... # Promise of a future build. Same as supplied in + # build.template.arguments[_IMAGE] + env: + - name: FOO + value: bar + - name: HELLO + value: world + + # serializes requests for function. Default value for functions + concurrencyModel: SingleThreaded + # max time allowed to respond to request + timeoutSeconds: 20 +``` + +Upon creating or updating the configuration, a new Revision is created +per the previous examples. Rollout operations are also identical to +the previous examples. diff --git a/docs/spec/overview.md b/docs/spec/overview.md new file mode 100644 index 000000000000..aed47c23fa90 --- /dev/null +++ b/docs/spec/overview.md @@ -0,0 +1,81 @@ +# Resource Types + +The primary resources in the Elafros API are Routes, Revisions, and Configurations: + +* A **Route** provides a named endpoint and a mechanism for routing traffic to + +* **Revisions**, which are immutable snapshots of code + config, created by a + +* **Configuration**, which acts as a stream of environments for Revisions. + +## Route + +**Route** provides a network endpoint for a user's service (which +consists of a series of software and configuration Revisions over +time). A kubernetes namespace may have multiple routes. The route +provides a long-lived, stable, named, HTTP-addressable endpoint that +is backed by one or more **Revisions**. The default configuration is +for the route to automatically route traffic to the latest revision +created by a **Configuration**. For more complex scenarios, the API +supports splitting traffic based on a percentage basis, and CI tools +could maintain multiple configurations for a single route +(e.g. "golden path" and “experiments”) or reference multiple revisions +directly to pin revisions during an incremental rollout and n-way +traffic split. The route can optionally assign addressable subdomains +to any or all backing revisions. + +**Revision** is an immutable snapshot of code and configuration. A +revision may have been created from a pre-built container image or +built from source. While there is a history of previous revisions, +only those currently referenced by a Route are addressable or +routable. Older inactive revisions need not be backed by underlying +resources, they may exist only as the revision metadata in +storage. Revisions are created by updates to a **Configuration**. + +A **Configuration** describes the desired latest Revision state, and +creates and tracks the status of Revisions as the desired state is +updated. A configuration may include instructions on how to transform +a source package (either git repo or archive) into a container by +referencing a [Build](https://github.com/elafros/build-crd), or may +simply reference a container image and associated execution metadata +needed by the Revision. On updates to a Configuration, a new build +and/or deployment (creating a Revision) may be performed; the +Configuration's controller will track the status of created Revisions +and makes both the most recently created and most recently *ready* +(i.e. healthy) Revision available in the status section. + +The system will be configured to not allow customer mutations to +Revisions. Instead, the creation of immutable Revisions through a +Configuration provides: + +* a single referenceable resource for the route to perform automated + rollouts + +* a single resource that can be watched to see a history of all + revisions created + +* enables (but doesn’t mandate) PATCH semantics for new revisions to + be done on the server, minimizing read-modify-write implemented + across multiple clients, which could result in optimistic + concurrency errors + +* the ability to rollback to a known good configuration + +In the conventional single live revision scenario, a route has a +single configuration with the same name as the route. Update +operations on the configuration enable scenarios such as: + +* *"Push code, keep config":* Specifying a new revision with updated + source, inheriting configuration such as env vars from the + configuration. + +* *"Update config, keep code"*: Specifying a new revision as just a + change to configuration, such as updating an env variable, + inheriting all other configuration and source/image. + +When creating an initial route and performing the first deployment, +the two operations of creating a Route and an associated Configuration +can be done in parallel, which streamlines the use case of deploying +code initially from a button. The +[sample API usage](normative_examples.md) section illustrates +conventional usage of the API. diff --git a/docs/spec/spec.md b/docs/spec/spec.md new file mode 100644 index 000000000000..620c6aa2f4f9 --- /dev/null +++ b/docs/spec/spec.md @@ -0,0 +1,249 @@ +## Resource Paths + +Resource paths in the Elafros API have the following standard k8s form: + +``` +/apis/{apiGroup}/{apiVersion}/namespaces/{metadata.namespace}/{kind}/{metadata.name} +``` + +For example: + +``` +/apis/elafros.dev/v1alpha1/namespaces/default/routes/my-service +``` + +It is expected that each Route will provide a name within a +cluster-wide DNS name. While no particular URL scheme is mandated +(consult the `domain` property of the Route for the authoritative +mapping), a common implementation would be to use the kubernetes +namespace mechanism to produce a URL like the following: + +``` +[$revisionname].$route.$namespace. +``` + +For example: + +``` +prod.my-service.default.mydomain.com +``` + + +# Resource YAML Definitions + +YAMLs for the Elafros API resources are described below, describing the +basic k8s structure: metadata, spec and status, along with comments on +specific fields. + +## Route + +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Route +metadata: + name: my-service + namespace: default + labels: + elafros.dev/type: ... # +optional convention: function|app + + # system generated meta + uid: ... +  resourceVersion: ... # used for optimistic concurrency control +  creationTimestamp: ... +  generation: ... # updated only when spec changes; used by observedGeneration +  selfLink: ... + ... +spec: + rollout: + traffic: + # list of oneof configurationName | revisionName. + # configurationName watches configurations to address latest latestReadyRevisionName + # revisionName pins a specific revision + - configurationName: ... + name: ... # +optional. Access as {name}.${status.domain}, + # e.g. oss: current.my-service.default.mydomain.com + percent: 100 # list percentages must add to 100. 0 is a valid list value + - ... + +status: + # domain: The hostname used to access the default (traffic-split) + # route. Typically, this will be composed of the name and namespace + # along with a cluster-specific prefix (here, mydomain.com). + domain: my-service.default.mydomain.com + + traffic: + # current rollout status list. configurationName references + # are dereferenced to latest revision + - revisionName: ... # latestReadyRevisionName from a configurationName in spec + name: ... + percent: ... # percentages add to 100. 0 is a valid list value + - ... + + conditions: + - type: RolloutInProgress + status: False + - type: TrafficDropped + status: False + - ... + + observedGeneration: ... # last generation being reconciled +``` + + +## Configuration + +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Configuration +metadata: + name: my-service + namespace: default + + # system generated meta + uid: ... +  resourceVersion: ... # used for optimistic concurrency control +  creationTimestamp: ... +  generation: ... # updated only when spec changes; used by observedGeneration +  selfLink: ... + ... +spec: + # +optional. composable Build spec, if omitted provide image directly + build: # This is a build.dev/v1alpha1.BuildTemplateSpec + source: + # oneof archive|manifest|repository: + + # +optional. + archive: + url: https://... + sha: ... + + # +optional. A manifest file containing a list of file paths, + # backing URLs, and sha checksums. Manifest may be a more + # efficient mechanism for a client to perform partial upload. + manifest + url: https://... + sha: ... + + # +optional. + repository: + url: https://github.com/jrandom/myrepo + commit: deadbeef + + template: # build template reference and arguments. + name: go_1_9_fn # builder name. Functions may have custom builders + namespace: build-templates + arguments: + - name: _IMAGE + value: gcr.io/... # destination for image + - name: _ENTRY_POINT + value: index # if function, language dependent entrypoint + + revisionTemplate: # template for building Revision + metadata: ... + labels: + elafros.dev/type: "function" # One of "function" or "app" + spec: # elafros.RevisionTemplateSpec. Copied to a new revision + + # +optional. if rolling back, the client may set this to the + # previous revision's build to avoid triggering a rebuild + buildName: ... + + # is a core.v1.Container; some fields not allowed, such as resources, ports + container: + # image either provided as pre-built container, or built by Elafros from + # source. When built by elafros, set to the same as build template, e.g. + # build.template.arguments[_IMAGE], as the "promise" of a future build + image: gcr.io/... + command: ['run'] + args: [] + env: + # list of environment vars + - name: FOO + value: bar + - name: HELLO + value: world + - ... + livenessProbe: ... # Optional + readinessProbe: ... # Optional + + # +optional concurrency strategy. SingleThreaded default value for functions + concurrencyModel: SingleThreaded + # +optional. max time the instance is allowed for responding to a request + timeoutSeconds: ... + serviceAccountName: ... # Name of the service account the code should run as. + +status: + # the latest created and ready to serve. Watched by route + latestReadyRevisionName: abc + # latest created revision, may still be in the process of being materialized + latestCreatedRevisionName: def + conditions: + - type: LatestRevisionReady + status: False + reason: ContainerMissing + message: "Unable to start because container is missing and build failed." + observedGeneration: ... # last generation being reconciled +``` + + +## Revision + +```yaml +apiVersion: elafros.dev/v1alpha1 +kind: Revision +metadata: + name: myservice-a1e34 # system generated + namespace: default + labels: + elafros.dev/configuration: ... # to list configurations/revisions by service + elafros.dev/configurationGeneration: ... # generation of configuration that created this Revision + elafros.dev/type: "function" # convention, one of "function" or "app" + # system generated meta + uid: ... +  resourceVersion: ... # used for optimistic concurrency control +  creationTimestamp: ... +  generation: ... +  selfLink: ... + ... + +# spec populated by Configuration +spec: + # +optional. name of the build.dev/v1alpha1.Build if built from source + buildName: ... + + container: # core.v1.Container + image: gcr.io/... + command: ['run'] + args: [] + env: # list of environment vars + - name: FOO + value: bar + - name: HELLO + value: world + - ... + livenessProbe: ... # Optional + readinessProbe: ... # Optional + concurrencyModel: ... + timeoutSeconds: ... + serviceAccountName: ... # Name of the service account the code should run as. + ... +status: + # This is a copy of metadata from the container image or grafeas, indicating + # the provenance of the revision, from the container + imageSource: + archive|manifest|repository: ... + conditions: + - type: Ready + status: False + message: "Starting Instances" + # if built from source: + - type: BuildComplete + status: True + # other conditions indicating build failure, if applicable + - ... + # URL for accessing the logs generated by this revision. Note that logs + # may still be access controlled separately from access to the API object. + logUrl: "logging.infra.mycompany.com/...?filter=revision=myservice-a1e34&..." +``` + + From abed4012332e5c8509fdc95fd8eea12b787a24d4 Mon Sep 17 00:00:00 2001 From: Evan Anderson Date: Sat, 24 Mar 2018 04:48:19 +0000 Subject: [PATCH 03/12] Add resource relationship images for overview and examples. --- docs/spec/images/auto_rollout.png | Bin 0 -> 13330 bytes docs/spec/images/build_example.png | Bin 0 -> 13941 bytes docs/spec/images/build_function.png | Bin 0 -> 14738 bytes docs/spec/images/initial_creation.png | Bin 0 -> 10811 bytes docs/spec/images/manual_rollout.png | Bin 0 -> 17399 bytes docs/spec/images/object_model.png | Bin 0 -> 11425 bytes docs/spec/normative_examples.md | 19 ++++++++++++++++--- docs/spec/overview.md | 2 ++ 8 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 docs/spec/images/auto_rollout.png create mode 100644 docs/spec/images/build_example.png create mode 100644 docs/spec/images/build_function.png create mode 100644 docs/spec/images/initial_creation.png create mode 100644 docs/spec/images/manual_rollout.png create mode 100644 docs/spec/images/object_model.png diff --git a/docs/spec/images/auto_rollout.png b/docs/spec/images/auto_rollout.png new file mode 100644 index 0000000000000000000000000000000000000000..04dfb62c4fd8f70d99775f5bb8a206fcd17928fa GIT binary patch literal 13330 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 YlAO`y7}5aXe|@fLsOTtHDcXenFD*|-^#A|> literal 0 HcmV?d00001 diff --git a/docs/spec/images/build_example.png b/docs/spec/images/build_example.png new file mode 100644 index 0000000000000000000000000000000000000000..28fb83e2e3bbdcf569d2df8fd49e9ada4f3b71e6 GIT binary patch literal 13941 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±*GTSm;-ArDQOdcwEH>*sX6 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 T_1(xoi~^>it6r-5F!=ufD|H8N literal 0 HcmV?d00001 diff --git a/docs/spec/images/object_model.png b/docs/spec/images/object_model.png new file mode 100644 index 0000000000000000000000000000000000000000..c48a4df5329c6d183e873ed86715714672c4b762 GIT binary patch literal 11425 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

Ax*)Dc@8FCtG11Py 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 QfwmwWEkn%`^_xNe4Os&?&j0`b literal 0 HcmV?d00001 diff --git a/docs/spec/normative_examples.md b/docs/spec/normative_examples.md index b9c21b769851..86c93408850f 100644 --- a/docs/spec/normative_examples.md +++ b/docs/spec/normative_examples.md @@ -42,7 +42,6 @@ $ elafros deploy --service my-service Deployed to https://my-service.default.mydomain.com ``` - **Steps**: * Update the Configuration with the config change @@ -52,8 +51,11 @@ $ elafros deploy --service my-service * A new Revision is created, and automatically rolled out to 100% once ready +![Automatic Rollout](auto_rollout.png) + + After the initial Route and Configuration have been created (which is -shown in the [second example]()), the typical +shown in the [second example](TODO)), the typical interaction is to update the revision configuration, resulting in the creation of a new revision, which will be automatically rolled out by the route. Revision configuration updates can be handled as either a @@ -245,6 +247,9 @@ $ elafros deploy --service my-service --region us-central1 * The route begins serving traffic to the revision that was created by the configuration +![Initial Creation](initial_creation.png) + + The previous example assumed an existing Route and Configuration to illustrate the common scenario of updating the configuration to deploy a new revision to the service. @@ -466,6 +471,9 @@ current,next 100% v3 2018-01-19 12:16 user1 64d79ce completing the rollout, the next revision is now the current revision +![Manual rollout](manual_rollout.png) + + In the previous examples, the route referenced a Configuration for automatic rollouts of new Revisions. While this pattern is useful for many scenarios such as functions-as-a-service and simple development @@ -705,6 +713,9 @@ $ elafros deploy --service my-service build and a new revision based on the template, and can be rolled out per earlier examples +![Build Example](build_example.png) + + Previous examples demonstrated configurations created with pre-built containers. Revisions can also be created by providing build information to the configuration, which results in a container image @@ -842,7 +853,6 @@ $ elafros deploy --function index --service my-function Deployed to https://my-function.default.mydomain.com ``` - **Steps**: * Create/Update a Configuration, additionally specifying function details. @@ -853,6 +863,9 @@ $ elafros deploy --function index --service my-function based on the template build and spec which can be rolled out per previous examples +![Build Function](build_function.png) + + Previous examples illustrated creating and deploying revisions in the context of apps. Functions are created and deployed in the same manner (in particular, as containers which respond to HTTP). In the diff --git a/docs/spec/overview.md b/docs/spec/overview.md index aed47c23fa90..6298517e8941 100644 --- a/docs/spec/overview.md +++ b/docs/spec/overview.md @@ -8,6 +8,8 @@ The primary resources in the Elafros API are Routes, Revisions, and Configuratio * **Configuration**, which acts as a stream of environments for Revisions. +![Object model](images/object_model.png) + ## Route **Route** provides a network endpoint for a user's service (which From 5f531b3f53b4a15271ffcd59235cd4a76759fd4d Mon Sep 17 00:00:00 2001 From: Evan Anderson Date: Sat, 24 Mar 2018 04:52:36 +0000 Subject: [PATCH 04/12] Fix image links to point to subdir --- docs/spec/normative_examples.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/spec/normative_examples.md b/docs/spec/normative_examples.md index 86c93408850f..d5524d8360f4 100644 --- a/docs/spec/normative_examples.md +++ b/docs/spec/normative_examples.md @@ -51,7 +51,7 @@ $ elafros deploy --service my-service * A new Revision is created, and automatically rolled out to 100% once ready -![Automatic Rollout](auto_rollout.png) +![Automatic Rollout](images/auto_rollout.png) After the initial Route and Configuration have been created (which is @@ -247,7 +247,7 @@ $ elafros deploy --service my-service --region us-central1 * The route begins serving traffic to the revision that was created by the configuration -![Initial Creation](initial_creation.png) +![Initial Creation](images/initial_creation.png) The previous example assumed an existing Route and Configuration to @@ -471,7 +471,7 @@ current,next 100% v3 2018-01-19 12:16 user1 64d79ce completing the rollout, the next revision is now the current revision -![Manual rollout](manual_rollout.png) +![Manual rollout](images/manual_rollout.png) In the previous examples, the route referenced a Configuration for @@ -713,7 +713,7 @@ $ elafros deploy --service my-service build and a new revision based on the template, and can be rolled out per earlier examples -![Build Example](build_example.png) +![Build Example](images/build_example.png) Previous examples demonstrated configurations created with pre-built @@ -863,7 +863,7 @@ $ elafros deploy --function index --service my-function based on the template build and spec which can be rolled out per previous examples -![Build Function](build_function.png) +![Build Function](images/build_function.png) Previous examples illustrated creating and deploying revisions in the From e4e16eaea52208177680269e6c3525119505f26f Mon Sep 17 00:00:00 2001 From: Evan Anderson Date: Sat, 24 Mar 2018 05:45:56 +0000 Subject: [PATCH 05/12] Update personas with @labadav feedback and additional words. --- docs/product/personas.md | 77 ++++++++++++++++++++++++++++++++++------ 1 file changed, 66 insertions(+), 11 deletions(-) diff --git a/docs/product/personas.md b/docs/product/personas.md index 7fe34bb2bca0..94f697a0f8a7 100644 --- a/docs/product/personas.md +++ b/docs/product/personas.md @@ -9,15 +9,22 @@ might want to do the action. ### Developer Personas -These are generally software engineres looking to build and run a -stateless software application, without concern about the underlying +The developer personas are software engineers looking to build and run +a stateless application without concern about the underlying infrastructure. * Hobbyist -* "Backend" SWE -* "Full stack" SWE +* Backend SWE +* Full stack SWE * SRE +User stories: +* Deploy some code +* Update environment +* Roll back the last change +* Debug an error in code +* Monitor my application + ### Operator Personas * Hobbyist / Contributor @@ -25,16 +32,64 @@ infrastructure. * Security Engineer / Auditor * Capacity Planner +User stories: +* Create an Elafros cluster +* Apply policy / RBAC +* Control or charge back for resource usage +* Choose logging or monitoring plugins + ## Elafros Build -* Developer - - Start a build - - Read build logs -* Language operator - - Create a build path/build pack +We expect the build components of Elafros to be useful on their own, +as well as in conjunction with the compute components. + +### Developer + +User stories: +* Start a build +* Read build logs + +### Language operator / contributor + +User stories: +* Create a build image / build pack + ## Elafros Events -* Event consumer (developer) -* Event producer \ No newline at end of file +Event generation and consumption is a core part of the serverless +(particularly function as a service) computing model. Event generation +and dispatch enables decoupling of event producers from consumers. + +## Event consumer (developer) + +User stories: +* Determine what event sources are available +* Trigger my service when certain events happen (event binding) +* Filter events from a provider + +## Event producer + +User stories: +* Publish events +* Control who can bind events + + +## Contributors + +Contributors are an important part of the Elafros project. As such, we +will also consider how various infrastructure encourages and enables +contributors to the project, as well as the impact on end-users. + +* Hobbyist or newcomer +* Motivated user +* Corporate (employed) maintainer +* Consultant + +User stories: +* Check out the code +* Build and run the code +* Run tests +* View test status +* Run performance tests From ddd3b5f9fad392af7106f9e65229f8bad38d3e02 Mon Sep 17 00:00:00 2001 From: Evan Anderson Date: Sat, 24 Mar 2018 05:46:50 +0000 Subject: [PATCH 06/12] Apply @labadav comments and other feedback on error defs, add ToC --- docs/spec/errors.md | 113 ++++++++++++++++++++++++-------------------- 1 file changed, 62 insertions(+), 51 deletions(-) diff --git a/docs/spec/errors.md b/docs/spec/errors.md index 62c3fdfb85d5..90a54ceaa9d9 100644 --- a/docs/spec/errors.md +++ b/docs/spec/errors.md @@ -1,30 +1,41 @@ # Error Conditions and Reporting -The standard kubernetes API pattern for reporting configuration errors -and current state of the system is to use the status section to report -the state of the system observed by the controller. There are two -mechanisms commonly used in status: +Elafros uses the standard Kubernetes API pattern for reporting +configuration errors and current state of the system by writing the +report in the `status` section. There are two mechanisms commonly used +in status: * conditions represent true/false statements about the current state of the resource. -* other fields may provide status on the most-recently retrieved state - of the world as it relates to the resource (example: number of +* other fields may provide status on the most recently retrieved state + of the system as it relates to the resource (example: number of replicas or traffic assignments). -Both of these mechanisms will often include additional data from the +Both of these mechanisms often include additional data from the controller such as `observedGeneration` (to determine whether the -controller has seen the latest updates to the spec). Below are some -example error scenarios (either due to user or system error), along -with how the status would be presented to CLI and UI tools via the -API: +controller has seen the latest updates to the spec). Example user and +system error scenarios are included below along with how the status is +presented to CLI and UI tools via the API. + +* [Revision failed to become Ready](#revision-failed-to-become-ready) +* [Build failed](#build-failed) +* [Revision not found by Route](#revision-not-found-by-route) +* [Configuration not found by Route](#configuration-not-found-by-route) +* [Latest Revision of a Configuration deleted](#latest-revision-of-a-configuration-deleted) +* [Resource exhausted while creating a revision](#resource-exhausted-while-creating-a-revision) +* [Deployment progressing slowly/stuck](#deployment-progressing-slowly-stuck) +* [Traffic shift progressing slowly/stuck](#traffic-shift-progressing-slowly-stuck) +* [Container image not present in repository](#container-image-not-present-in-repository) +* [Container image fails at startup on Revision](#container-image-fails-at-startup-on-revision) + ## Revision failed to become Ready -If the latest Revision fails to become "Ready" within some reasonable -timeframe (for whatever reason), the Configuration should signal this -with the `LatestRevisionReady` status, copying the reason and message -from the Ready condition on the Revision. +If the latest Revision fails to become `Ready` for any reason within some reasonable +timeframe, the Configuration should signal this +with the `LatestRevisionReady` status, copying the reason and the message +from the `Ready` condition on the Revision. ```yaml ... @@ -41,12 +52,12 @@ status: ## Build failed -If the Build steps failed while creating a Revision, this would be -detectable by examining the Failed condition on the Build or the -BuildFailed condition on the Revision (which copies the value from the -build referenced by `spec.buildName`). In addition, the Build resource -(but not the Revision) should have a status field to link to the log -output of the build. +If the Build steps failed while creating a Revision, you can examine +the `Failed` condition on the Build or the `BuildFailed` condition on +the Revision (which copies the value from the build referenced by +`spec.buildName`). In addition, the Build resource (but not the +Revision) should have a status field to link to the log output of the +build. ```http GET /apis/build.dev/v1alpha1/namespaces/default/builds/build-1acub3 @@ -62,7 +73,7 @@ status: - type: BuildFailed status: True reason: BuildStepFailed # could also be SourceMissing, etc - # reason is for machine consumption, message is for human consumption + # reason is a short status, message provides error details message: "Step XYZ failed with error message: $LASTLOGLINE" ``` @@ -89,8 +100,8 @@ status: ## Revision not found by Route If a Revision is referenced in the Route's `spec.rollout.traffic`, the -corresponding entry in the `status.traffic` list will be set to the name -"Not found", and the `TrafficDropped` condition would be marked as True, +corresponding entry in the `status.traffic` list will be set to "Not +found", and the `TrafficDropped` condition will be marked as True, with a reason of `RevisionMissing`. ```http @@ -121,8 +132,8 @@ status: If a Route references the `latestReadyRevisionName` of a Configuration and the Configuration cannot be found, the corresponding entry in -`status.traffic` list will be set to the name "Not found", and the -`TrafficDropped` condition would be marked as True with a reason of +`status.traffic` list will be set to "Not found", and the +`TrafficDropped` condition will be marked as True with a reason of `ConfigurationMissing`. ```http @@ -149,7 +160,7 @@ status: If the most recent (or most recently ready) Revision is deleted, the Configuration will clear the `latestReadyRevisionName`. If the -Configuration is referenced by a Route, then the Route will set the +Configuration is referenced by a Route, the Route will set the `TrafficDropped` condition with reason `RevisionMissing`, as above. ```http @@ -171,10 +182,10 @@ status: ## Resource exhausted while creating a revision Since a Revision is only metadata, the Revision will be created, but -may have a condition indicating the underlying failure, possibly -including the associated underlying resource. In a multitenant -environment, the customer may not have have access or visibility into -the underlying resources in the hosting environment. +will have a condition indicating the underlying failure, possibly +indicating the failed underlying resource. In a multitenant +environment, the customer might not have have access or visibility +into the underlying resources in the hosting environment. ```http GET /apis/elafros.dev/v1alpha1/namespaces/default/revisions/abc @@ -194,17 +205,17 @@ status: See [the kubernetes documentation for how this is handled for Deployments](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#failed-deployment). For -Revisions, we would start by assuming a single timeout for deployment +Revisions, we will start by assuming a single timeout for deployment (rather than configurable), and report that the Revision was not -Ready, with a reason `ProgressDeadlineExceeded`. Note that we would only -report `ProgressDeadlineExceeded` if we could not determine another -reason (such as quota failures, missing build, or container execution -failures). +Ready, with a reason `ProgressDeadlineExceeded`. Note that we will +only report `ProgressDeadlineExceeded` if we could not determine +another reason (such as quota failures, missing build, or container +execution failures). Kubernetes controllers will continue attempting to make progress (possibly at a less-aggressive rate) when they encounter a case where the desired status cannot match the actual status, so if the -underlying deployment is slow, it may eventually finish after +underlying deployment is slow, it might eventually finish after reporting `ProgressDeadlineExceeded`. ```http @@ -224,9 +235,9 @@ status: ## Traffic shift progressing slowly/stuck Similar to deployment slowness, if the transfer of traffic (either via -gradual rollout or knife-switch) takes longer than a certain timeout -to complete/update, the `RolloutInProgress` condition would remain at -true, but the reason would be set to `ProgressDeadlineExceeded`. +gradual or abrupt rollout) takes longer than a certain timeout to +complete/update, the `RolloutInProgress` condition will remain at +True, but the reason will be set to `ProgressDeadlineExceeded`. ```http GET /apis/elafros.dev/v1alpha1/namespaces/default/routes/abc @@ -250,17 +261,17 @@ status: ## Container image not present in repository -We expect that Revisions may be created while a Build is still -creating the container image or uploading it to the repository. If the -build is being performed by a CRD in the cluster, the spec.buildName -attribute will be set (and see the "Build failed" example). In other -cases when the build is not supplied, the container image referenced -may not be present in the registry (either because of a typo or -because it was deleted). In this case, the Ready condition will be set -to False with a reason of ContainerMissing. This condition could be -corrected if the image becomes available at a later time. We may also -make a defensive copy of the container image (e.g. in Riptide) to -avoid this error due to deleted source container. +Revisions might be created while a Build is still creating the +container image or uploading it to the repository. If the build is +being performed by a CRD in the cluster, the spec.buildName attribute +will be set (and see the [Build failed](#build-failed) example). In +other cases when the build is not supplied, the container image +referenced might not be present in the registry (either because of a +typo or because it was deleted). In this case, the Ready condition +will be set to False with a reason of ContainerMissing. This condition +could be corrected if the image becomes available at a later time. We +can also make a defensive copy of the container image to avoid this +error due to deleted source container. ```http GET /apis/elafros.dev/v1alpha1/namespaces/default/revisions/abc From 21f3d1cd74c8ec17dd1756835775b07815b33854 Mon Sep 17 00:00:00 2001 From: Evan Anderson Date: Sat, 24 Mar 2018 05:48:17 +0000 Subject: [PATCH 07/12] Apply @sixolet and @cooperneil feedback on types and resource structure. --- docs/spec/normative_examples.md | 20 ++++++------- docs/spec/overview.md | 8 +++--- docs/spec/spec.md | 51 +++++++++++++++++---------------- 3 files changed, 39 insertions(+), 40 deletions(-) diff --git a/docs/spec/normative_examples.md b/docs/spec/normative_examples.md index d5524d8360f4..aa9337b91394 100644 --- a/docs/spec/normative_examples.md +++ b/docs/spec/normative_examples.md @@ -184,8 +184,8 @@ status: - revisionName: def percent: 25 conditions: - - type: RolloutInProgress - status: True + - type: RolloutComplete + status: False ``` And once reconciled, revision def serves 100% of the traffic : @@ -210,8 +210,8 @@ status: - revisionName: def percent: 100 conditions: - - type: RolloutInProgress - status: False + - type: RolloutComplete + status: True ... ``` @@ -413,8 +413,8 @@ status: percent: 100 conditions: - - type: RolloutInProgress - status: False + - type: RolloutComplete + status: True observedGeneration: 2145 ``` @@ -602,8 +602,8 @@ status: name: next # addressable as next.my-service.default.mydomain.com percent: 0 conditions: - - type: RolloutInProgress - status: False # Underlying routes as set in config. + - type: RolloutComplete + status: True ... ``` @@ -654,9 +654,7 @@ status: name: next percent: 100 conditions: - - type: RolloutInProgress - status: False - - type: RolloutAtTargetPercentage + - type: RolloutComplete status: True ... ``` diff --git a/docs/spec/overview.md b/docs/spec/overview.md index 6298517e8941..8c9b73d50aab 100644 --- a/docs/spec/overview.md +++ b/docs/spec/overview.md @@ -56,10 +56,10 @@ Configuration provides: * a single resource that can be watched to see a history of all revisions created -* enables (but doesn’t mandate) PATCH semantics for new revisions to - be done on the server, minimizing read-modify-write implemented - across multiple clients, which could result in optimistic - concurrency errors +* (but doesn’t mandate) PATCH semantics for new revisions to be done + on the server, minimizing read-modify-write implemented across + multiple clients, which could result in optimistic concurrency + errors * the ability to rollback to a known good configuration diff --git a/docs/spec/spec.md b/docs/spec/spec.md index 620c6aa2f4f9..43c90d61b31f 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -54,16 +54,15 @@ metadata:   selfLink: ... ... spec: - rollout: - traffic: - # list of oneof configurationName | revisionName. - # configurationName watches configurations to address latest latestReadyRevisionName - # revisionName pins a specific revision - - configurationName: ... - name: ... # +optional. Access as {name}.${status.domain}, - # e.g. oss: current.my-service.default.mydomain.com - percent: 100 # list percentages must add to 100. 0 is a valid list value - - ... + traffic: + # list of oneof configurationName | revisionName. + # configurationName watches configurations to address latest latestReadyRevisionName + # revisionName pins a specific revision + - configurationName: ... + name: ... # +optional. Access as {name}.${status.domain}, + # e.g. oss: current.my-service.default.mydomain.com + percent: 100 # list percentages must add to 100. 0 is a valid list value + - ... status: # domain: The hostname used to access the default (traffic-split) @@ -110,24 +109,26 @@ spec: # +optional. composable Build spec, if omitted provide image directly build: # This is a build.dev/v1alpha1.BuildTemplateSpec source: - # oneof archive|manifest|repository: + # oneof git|gcs|custom: # +optional. - archive: - url: https://... - sha: ... - - # +optional. A manifest file containing a list of file paths, - # backing URLs, and sha checksums. Manifest may be a more - # efficient mechanism for a client to perform partial upload. - manifest - url: https://... - sha: ... - - # +optional. - repository: + git: url: https://github.com/jrandom/myrepo - commit: deadbeef + commit: deadbeef # Or branch, tag, ref + + # +optional. A zip archive or a manifest file in Google Cloud + # Storage. A manifest file is a file containing a list of file + # paths, backing URLs, and sha checksums. Manifest may be a more + # efficient mechanism for a client to perform partial upload. + gcs: + location: https://... + type: 'archive' # Or 'manifest' + + # +optional. Custom specifies a container which will be run as + # the first build step to fetch the source. + custom: # is a core.v1.Container + image: gcr.io/cloud-builders/git:latest + args: [ "clone", "https://...", "other-place" ] template: # build template reference and arguments. name: go_1_9_fn # builder name. Functions may have custom builders From c93a524ad698752f3f6306c97566fcacdf0d5174 Mon Sep 17 00:00:00 2001 From: Evan Anderson Date: Sat, 24 Mar 2018 05:54:39 +0000 Subject: [PATCH 08/12] Apply remaining @labadav comments. --- docs/spec/motivation.md | 21 +++++++++++---------- docs/spec/overview.md | 20 ++++++++++---------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/docs/spec/motivation.md b/docs/spec/motivation.md index d683600731bc..508a4dedd364 100644 --- a/docs/spec/motivation.md +++ b/docs/spec/motivation.md @@ -1,21 +1,22 @@ The goal of the Elafros project is to provide a common toolkit and API framework for serverless workloads. -We define serverless workloads as computing workloads which are: +We define serverless workloads as computing workloads that are: -* Stateless. -* Amenable to the process scale-out model. +* Stateless +* Amenable to the process scale-out model * Primarily driven by application level (L7 -- HTTP, for example) - request traffic. + request traffic While Kubernetes provides basic primitives like Deployment, Service, and Ingress in support of this model, our experience suggests that a more compact and richer opinionated model has substantial benefit for developers. In particular, by standardizing on higher-level primitives -which perform substantial amounts of automation of common primitives, -it should be possible to build consistent toolkits which provide a -richer experience than via yaml files with `kubectl`. +which perform substantial amounts of automation of common +infrastructure, it should be possible to build consistent toolkits +that provide a richer experience than updating yaml files with +`kubectl`. -The Elafos APIs are composed of three facets; this document primarily -concerns itself with the compute API; the other two APIs are Build and -Eventing. +The Elafos APIs consist of Compute API (these documents), +[Build API](https://github.com/elafros/build) and +[Eventing API](https://github.com/elafros/eventing). diff --git a/docs/spec/overview.md b/docs/spec/overview.md index 8c9b73d50aab..1d51825026ec 100644 --- a/docs/spec/overview.md +++ b/docs/spec/overview.md @@ -14,7 +14,7 @@ The primary resources in the Elafros API are Routes, Revisions, and Configuratio **Route** provides a network endpoint for a user's service (which consists of a series of software and configuration Revisions over -time). A kubernetes namespace may have multiple routes. The route +time). A kubernetes namespace can have multiple routes. The route provides a long-lived, stable, named, HTTP-addressable endpoint that is backed by one or more **Revisions**. The default configuration is for the route to automatically route traffic to the latest revision @@ -27,18 +27,18 @@ traffic split. The route can optionally assign addressable subdomains to any or all backing revisions. **Revision** is an immutable snapshot of code and configuration. A -revision may have been created from a pre-built container image or -built from source. While there is a history of previous revisions, -only those currently referenced by a Route are addressable or -routable. Older inactive revisions need not be backed by underlying -resources, they may exist only as the revision metadata in -storage. Revisions are created by updates to a **Configuration**. +revision can be created from a pre-built container image or built from +source. While there is a history of previous revisions, only those +currently referenced by a Route are addressable or routable. Older +inactive revisions need not be backed by underlying resources, they +exist only as the revision metadata in storage. Revisions are created +by updates to a **Configuration**. A **Configuration** describes the desired latest Revision state, and creates and tracks the status of Revisions as the desired state is -updated. A configuration may include instructions on how to transform +updated. A configuration might include instructions on how to transform a source package (either git repo or archive) into a container by -referencing a [Build](https://github.com/elafros/build-crd), or may +referencing a [Build](https://github.com/elafros/build), or might simply reference a container image and associated execution metadata needed by the Revision. On updates to a Configuration, a new build and/or deployment (creating a Revision) may be performed; the @@ -53,7 +53,7 @@ Configuration provides: * a single referenceable resource for the route to perform automated rollouts -* a single resource that can be watched to see a history of all +* a single resource that can be watched to see a history of all the revisions created * (but doesn’t mandate) PATCH semantics for new revisions to be done From 99b541ddc3dedbe3d282cfec70677cff9e2d813f Mon Sep 17 00:00:00 2001 From: Evan Anderson Date: Sat, 24 Mar 2018 15:22:05 +0000 Subject: [PATCH 09/12] Address @mattmoor comments. --- docs/spec/errors.md | 23 +++++--- docs/spec/motivation.md | 2 +- docs/spec/normative_examples.md | 95 ++++++++++++++++----------------- docs/spec/overview.md | 16 +++--- docs/spec/spec.md | 16 +++--- 5 files changed, 76 insertions(+), 76 deletions(-) diff --git a/docs/spec/errors.md b/docs/spec/errors.md index 90a54ceaa9d9..7dcacf7d1ab7 100644 --- a/docs/spec/errors.md +++ b/docs/spec/errors.md @@ -41,7 +41,7 @@ from the `Ready` condition on the Revision. ... status: latestReadyRevisionName: abc - latestCreatedRevisionName: bcd # Hasn't become became "Ready" + latestCreatedRevisionName: bcd # Hasn't become "Ready" conditions: - type: LatestRevisionReady status: False @@ -68,9 +68,7 @@ status: # Link to log stream; could be ELK or Stackdriver, for example buildLogsLink: "http://logging.infra.mycompany.com/...?filter=..." conditions: - - type: Complete - status: True - - type: BuildFailed + - type: Failed status: True reason: BuildStepFailed # could also be SourceMissing, etc # reason is a short status, message provides error details @@ -92,7 +90,7 @@ status: - type: BuildFailed status: True reason: BuildStepFailed - # reason is for machine consumption, message is for human consumption + # reason is a short status, message provides error details message: "Step XYZ failed with error message: $LASTLOGLINE" ``` @@ -123,7 +121,7 @@ status: - type: TrafficDropped status: True reason: RevisionMissing - # reason is for machine consumption, message is for human consumption + # reason is a short status, message provides error details message: "Revision 'qyzz' referenced in rollout.traffic not found" ``` @@ -151,7 +149,7 @@ status: - type: TrafficDropped status: True reason: ConfigurationMissing - # reason is for machine consumption, message is for human consumption + # reason is a short status, message provides error details message: "Revision 'my-service' referenced in rollout.traffic not found" ``` @@ -175,6 +173,11 @@ spec: ... status: latestCreatedRevision: abc + conditions: + - type: LatestRevisionReady + status: False + reason: RevisionMissing + message: "The latest Revision appears to have been deleted." observedGeneration: 1234 ``` @@ -254,7 +257,7 @@ status: - type: RolloutInProgress status: True reason: ProgressDeadlineExceeded - # reason is for machine consumption, message is for human consumption + # reason is a short status, message provides error details message: "Unable to update traffic split for more than 120 seconds." ``` @@ -284,6 +287,10 @@ status: status: False reason: ContainerMissing message: "Unable to fetch image 'gcr.io/...': " + - type: Failed + status: True + reason: ContainerMissing + message: "Unable to fetch image 'gcr.io/...': " ``` diff --git a/docs/spec/motivation.md b/docs/spec/motivation.md index 508a4dedd364..573e6da9bab5 100644 --- a/docs/spec/motivation.md +++ b/docs/spec/motivation.md @@ -17,6 +17,6 @@ infrastructure, it should be possible to build consistent toolkits that provide a richer experience than updating yaml files with `kubectl`. -The Elafos APIs consist of Compute API (these documents), +The Elafros APIs consist of Compute API (these documents), [Build API](https://github.com/elafros/build) and [Eventing API](https://github.com/elafros/eventing). diff --git a/docs/spec/normative_examples.md b/docs/spec/normative_examples.md index aa9337b91394..8b2aac4bf612 100644 --- a/docs/spec/normative_examples.md +++ b/docs/spec/normative_examples.md @@ -6,20 +6,14 @@ building from the smallest, most frequent operations. Examples in this section illustrate: -* Creating a first route to deploy a first revision from a pre-built - container - -* Automatic and manual rollout options for deploying subsequent - revisions - -* Creating a revision from source - -* Creating a function - -Additional common scenarios, including rollback and listing resources, -are shown in [Appendix A](additional_examples.md). More sophisticated -rollout scenarios that the API may support, including n-way traffic -splitting, are shown in [Appendix B](complex_examples.md). +* [Automatic rollout of a new Revision to an existing Service with a + pre-built container](#1--automatic-rollout-of-a-new-revision-to-existing-service---pre-built-container) +* [Creating a first route to deploy a first revision from a pre-built + container](#2--creating-route-and-deploying-first-revision---pre-built-container) +* [Configuration changes and manual rollout + options](#3--manual-rollout-of-a-new-revision---config-change-only) +* [Creating a revision from source](#4--deploy-a-revision-from-source) +* [Creating a function from source](#5--deploy-a-function) Note that these API operations are identical for both app and function based services. (to see the full resource definitions, see the @@ -80,7 +74,7 @@ new container image, inheriting previous configuration from the configuration: ```http -PATCH /apis/elafros.dev/namespaces/default/configurations/my-service +PATCH /apis/elafros.dev/v1alpha1/namespaces/default/configurations/my-service ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -94,11 +88,11 @@ spec: image: gcr.io/... # new image ``` -The update to the configuration triggers a new revision being created, -and the configuration is updated to reflect the new revision: +The update to the Configuration triggers a new revision being created, +and the Configuration is updated to reflect the new Revision: ```http -GET /apis/elafros.dev/namespaces/default/configurations/my-service +GET /apis/elafros.dev/v1alpha1/namespaces/default/configurations/my-service ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -122,7 +116,7 @@ new generation of the configuration (1235), indicating the provenance of the revision: ```http -GET /apis/elafros.dev/namespaces/default/revisions/def +GET /apis/elafros.dev/v1alpha1/namespaces/default/revisions/def ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -159,7 +153,7 @@ to it. During reconciliation, traffic may be routed to both existing revision `abc` and new revision `def`: ```http -GET /apis/elafros.dev/namespaces/default/routes/my-service +GET /apis/elafros.dev/v1alpha1/namespaces/default/routes/my-service ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -191,7 +185,7 @@ status: And once reconciled, revision def serves 100% of the traffic : ```http -GET /apis/elafros.dev/namespaces/default/routes/my-service +GET /apis/elafros.dev/v1alpha1/namespaces/default/routes/my-service ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -275,7 +269,7 @@ The client creates the route and configuration, which by convention share the same name: ```http -POST /apis/elafros.dev/namespaces/default/routes +POST /apis/elafros.dev/v1alpha1/namespaces/default/routes ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -290,7 +284,7 @@ spec: ``` ```http -POST /apis/elafros.dev/namespaces/default/configurations +POST /apis/elafros.dev/v1alpha1/namespaces/default/configurations ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -318,7 +312,7 @@ Revision, generating its name, and applying the spec and metadata from the configuration, as well as new metadata labels: ```http -GET /apis/elafros.dev/namespaces/default/revisions/abc +GET /apis/elafros.dev/v1alpha1/namespaces/default/revisions/abc ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -344,7 +338,7 @@ resources have been fully materialized, the configuration is updated with latestCreatedRevisionName: ```http -GET /apis/elafros.dev/namespaces/default/configurations/my-service +GET /apis/elafros.dev/v1alpha1/namespaces/default/configurations/my-service ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -365,7 +359,7 @@ The configuration watches the revision, and when the revision is updated as Ready (to serve), the latestReadyRevisionName is updated: ```http -GET /apis/elafros.dev/namespaces/default/configurations/my-service +GET /apis/elafros.dev/v1alpha1/namespaces/default/configurations/my-service ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -390,7 +384,7 @@ new revision `abc`, addressable as `my-service.default.mydomain.com`. Once reconciled: ```http -GET /apis/elafros.dev/namespaces/default/routes/my-service +GET /apis/elafros.dev/v1alpha1/namespaces/default/routes/my-service ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -433,8 +427,8 @@ $ elafros deploy --service my-service --env HELLO="blurg" [...] $ elafros revisions list --service my-service -Name Traffic Id Date Deployer Git sha -next 0% v3 2018-01-19 12:16 user1 64d79ce +Name Traffic Id Date Deployer Git SHA +next 0% v3 2018-01-19 12:16 user1 a6f92d1 current 100% v2 2018-01-18 20:34 user1 a6f92d1 v1 2018-01-17 10:32 user1 33643fc @@ -446,8 +440,8 @@ $ elafros rollout finish [...] $ elafros revisions list --service my-service -Name Traffic Id Date Deployer Git sha -current,next 100% v3 2018-01-19 12:16 user1 64d79ce +Name Traffic Id Date Deployer Git SHA +current,next 100% v3 2018-01-19 12:16 user1 a6f92d1 v2 2018-01-18 20:34 user1 a6f92d1 v1 2018-01-17 10:32 user1 33643fc ``` @@ -486,7 +480,7 @@ semi-automatic variation of manual rollouts). The client updates the route to pin the current revision: ```http -PATCH /apis/elafros.dev/namespaces/default/routes/my-service +PATCH /apis/elafros.dev/v1alpha1/namespaces/default/routes/my-service ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -505,7 +499,7 @@ the creation of a new revision, in this case updating the container image but keeping the same config: ```http -PATCH /apis/elafros.dev/namespaces/default/configurations/my-service +PATCH /apis/elafros.dev/v1alpha1/namespaces/default/configurations/my-service ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -525,7 +519,7 @@ A new revision `ghi` is created that has the same code as the previous revision `def`, but different config: ```http -GET /apis/elafros.dev/namespaces/default/revisions/ghi +GET /apis/elafros.dev/v1alpha1/namespaces/default/revisions/ghi ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -557,7 +551,7 @@ revision at 0% traffic but making it addressable through subdomain `next`: ```http -PATCH /apis/elafros.dev/namespaces/default/routes/my-service +PATCH /apis/elafros.dev/v1alpha1/namespaces/default/routes/my-service ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -582,7 +576,7 @@ the names current/next have semantic meaning, they are convention only; blue/green, or any other subdomain names could be configured. ```http -GET /apis/elafros.dev/namespaces/default/routes/my-service +GET /apis/elafros.dev/v1alpha1/namespaces/default/routes/my-service ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -613,7 +607,7 @@ After testing the new revision at totaling 100%): ```http -PATCH /apis/elafros.dev/namespaces/default/routes/my-service +PATCH /apis/elafros.dev/v1alpha1/namespaces/default/routes/my-service ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -634,7 +628,7 @@ spec: After reconciliation, all traffic has been shifted to the new version: ```http -GET /apis/elafros.dev/namespaces/default/routes/my-service +GET /apis/elafros.dev/v1alpha1/namespaces/default/routes/my-service ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -665,7 +659,7 @@ left addressing the same revision as current so that `next.my-service.default.mydomain.com` is always addressable. ```http -PATCH /apis/elafros.dev/namespaces/default/routes/my-service +PATCH /apis/elafros.dev/v1alpha1/namespaces/default/routes/my-service ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -735,7 +729,7 @@ The client creates the configuration inlining a build spec for an archive based source build, and referencing a nodejs build template: ```http -POST /apis/elafros.dev/namespaces/default/configurations +POST /apis/elafros.dev/v1alpha1/namespaces/default/configurations ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -745,10 +739,10 @@ metadata: spec: build: # build.dev/v1alpha1.BuildTemplateSpec source: - # oneof archive|manifest|repository: - archive: + # oneof git|gcs|custom: + git: url: https://... - sha: ... + commit: ... template: # defines build template name: nodejs_8_9_4 # builder name namespace: build-templates @@ -776,8 +770,9 @@ updating the `revisionTemplate.spec.container.image` at the completion of the build, an update to both source and config could result in the creation of two Revisions, one with the config change, and the other with the new code deployment. It is expected that Revision will wait -for the `revisionTemplate.spec.container.image` to be live before -marking the Revision as "ready". +for the `buildName` to be complete and the +`revisionTemplate.spec.container.image` to be live before marking the +Revision as "ready". Upon creating/updating the configuration's build field, the system creates a new revision. The configuration controller will initiate a @@ -787,7 +782,7 @@ controller observes through the build reference, the high-level state of the build is mirrored into conditions in the Revision’s status: ```http -GET /apis/elafros.dev/namespaces/default/revisions/abc +GET /apis/elafros.dev/v1alpha1/namespaces/default/revisions/abc ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -885,7 +880,7 @@ not a core function of the compute API. Creating the configuration with build and function metadata: ```http -POST /apis/elafros.dev/namespaces/default/configurations +POST /apis/elafros.dev/v1alpha1/namespaces/default/configurations ``` ```yaml apiVersion: elafros.dev/v1alpha1 @@ -895,10 +890,10 @@ metadata: spec: build: # build.dev/v1alpha1.BuildTemplateSpec source: - # oneof archive|manifest|repository: - archive: + # oneof git|gcs|custom + git: url: https://... - sha: ... + commit: ... template: # defines build template name: go_1_9_fn # function builder namespace: build-templates diff --git a/docs/spec/overview.md b/docs/spec/overview.md index 1d51825026ec..4b56f34df24a 100644 --- a/docs/spec/overview.md +++ b/docs/spec/overview.md @@ -19,12 +19,12 @@ provides a long-lived, stable, named, HTTP-addressable endpoint that is backed by one or more **Revisions**. The default configuration is for the route to automatically route traffic to the latest revision created by a **Configuration**. For more complex scenarios, the API -supports splitting traffic based on a percentage basis, and CI tools -could maintain multiple configurations for a single route -(e.g. "golden path" and “experiments”) or reference multiple revisions -directly to pin revisions during an incremental rollout and n-way -traffic split. The route can optionally assign addressable subdomains -to any or all backing revisions. +supports splitting traffic on a percentage basis, and CI tools could +maintain multiple configurations for a single route (e.g. "golden +path" and “experiments”) or reference multiple revisions directly to +pin revisions during an incremental rollout and n-way traffic +split. The route can optionally assign addressable subdomains to any +or all backing revisions. **Revision** is an immutable snapshot of code and configuration. A revision can be created from a pre-built container image or built from @@ -52,15 +52,12 @@ Configuration provides: * a single referenceable resource for the route to perform automated rollouts - * a single resource that can be watched to see a history of all the revisions created - * (but doesn’t mandate) PATCH semantics for new revisions to be done on the server, minimizing read-modify-write implemented across multiple clients, which could result in optimistic concurrency errors - * the ability to rollback to a known good configuration In the conventional single live revision scenario, a route has a @@ -70,7 +67,6 @@ operations on the configuration enable scenarios such as: * *"Push code, keep config":* Specifying a new revision with updated source, inheriting configuration such as env vars from the configuration. - * *"Update config, keep code"*: Specifying a new revision as just a change to configuration, such as updating an env variable, inheriting all other configuration and source/image. diff --git a/docs/spec/spec.md b/docs/spec/spec.md index 43c90d61b31f..d8e846d72dd0 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -78,9 +78,9 @@ status: percent: ... # percentages add to 100. 0 is a valid list value - ... - conditions: - - type: RolloutInProgress - status: False + conditions: # See also the [error conditions documentation](errors.md) + - type: RolloutComplete + status: True - type: TrafficDropped status: False - ... @@ -150,10 +150,12 @@ spec: buildName: ... # is a core.v1.Container; some fields not allowed, such as resources, ports - container: + container: # image either provided as pre-built container, or built by Elafros from # source. When built by elafros, set to the same as build template, e.g. - # build.template.arguments[_IMAGE], as the "promise" of a future build + # build.template.arguments[_IMAGE], as the "promise" of a future build. + # If buildName is provided, it is expected that this image will be + # present when the referenced build is complete. image: gcr.io/... command: ['run'] args: [] @@ -178,7 +180,7 @@ status: latestReadyRevisionName: abc # latest created revision, may still be in the process of being materialized latestCreatedRevisionName: def - conditions: + conditions: # See also the [error conditions documentation](errors.md) - type: LatestRevisionReady status: False reason: ContainerMissing @@ -233,7 +235,7 @@ status: # the provenance of the revision, from the container imageSource: archive|manifest|repository: ... - conditions: + conditions: # See also the [error conditions documentation](errors.md) - type: Ready status: False message: "Starting Instances" From c2a765ea65bfd81c5710528889c2ee200939ba2f Mon Sep 17 00:00:00 2001 From: Evan Anderson Date: Sun, 25 Mar 2018 01:46:37 +0000 Subject: [PATCH 10/12] Fix personas link to be less implicitly biased. --- docs/product/personas.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/product/personas.md b/docs/product/personas.md index 94f697a0f8a7..910c3181245e 100644 --- a/docs/product/personas.md +++ b/docs/product/personas.md @@ -1,7 +1,7 @@ # Elafros Personas When discussing user actions, it is often helpful to [define specific -user roles](http://www.agilemodeling.com/artifacts/personas.htm) who +user roles](https://en.wikipedia.org/wiki/Persona_(user_experience)) who might want to do the action. @@ -93,3 +93,4 @@ User stories: * Run tests * View test status * Run performance tests + From 85458d514c41207d157a7f9dabd6e4639cc5403d Mon Sep 17 00:00:00 2001 From: Evan Anderson Date: Sun, 25 Mar 2018 01:47:08 +0000 Subject: [PATCH 11/12] Add section heads for Overview, to allow references later. --- docs/spec/overview.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/spec/overview.md b/docs/spec/overview.md index 4b56f34df24a..f8bd77de8f02 100644 --- a/docs/spec/overview.md +++ b/docs/spec/overview.md @@ -26,6 +26,8 @@ pin revisions during an incremental rollout and n-way traffic split. The route can optionally assign addressable subdomains to any or all backing revisions. +## Revision + **Revision** is an immutable snapshot of code and configuration. A revision can be created from a pre-built container image or built from source. While there is a history of previous revisions, only those @@ -34,6 +36,8 @@ inactive revisions need not be backed by underlying resources, they exist only as the revision metadata in storage. Revisions are created by updates to a **Configuration**. +## Configuration + A **Configuration** describes the desired latest Revision state, and creates and tracks the status of Revisions as the desired state is updated. A configuration might include instructions on how to transform @@ -46,6 +50,9 @@ Configuration's controller will track the status of created Revisions and makes both the most recently created and most recently *ready* (i.e. healthy) Revision available in the status section. + +# Orchestration + The system will be configured to not allow customer mutations to Revisions. Instead, the creation of immutable Revisions through a Configuration provides: From e41e0f6e72a2eeef461962979ff6fabf5279e622 Mon Sep 17 00:00:00 2001 From: Evan Anderson Date: Sun, 25 Mar 2018 01:59:28 +0000 Subject: [PATCH 12/12] Clean up tabs and other formatting in spec and errors --- docs/spec/errors.md | 4 ++-- docs/spec/spec.md | 27 +++++++++++++++++++-------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/docs/spec/errors.md b/docs/spec/errors.md index 7dcacf7d1ab7..5f0f3badf665 100644 --- a/docs/spec/errors.md +++ b/docs/spec/errors.md @@ -176,8 +176,8 @@ status: conditions: - type: LatestRevisionReady status: False - reason: RevisionMissing - message: "The latest Revision appears to have been deleted." + reason: RevisionMissing + message: "The latest Revision appears to have been deleted." observedGeneration: 1234 ``` diff --git a/docs/spec/spec.md b/docs/spec/spec.md index d8e846d72dd0..f6d6ab8256fd 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -37,6 +37,9 @@ specific fields. ## Route +For a high-level description of Routes, +[see the overview](overview.md#route). + ```yaml apiVersion: elafros.dev/v1alpha1 kind: Route @@ -91,6 +94,10 @@ status: ## Configuration +For a high-level description of Configurations, +[see the overview](overview.md#configuration). + + ```yaml apiVersion: elafros.dev/v1alpha1 kind: Configuration @@ -114,7 +121,7 @@ spec: # +optional. git: url: https://github.com/jrandom/myrepo - commit: deadbeef # Or branch, tag, ref + commit: deadbeef # Or branch, tag, ref # +optional. A zip archive or a manifest file in Google Cloud # Storage. A manifest file is a file containing a list of file @@ -126,7 +133,7 @@ spec: # +optional. Custom specifies a container which will be run as # the first build step to fetch the source. - custom: # is a core.v1.Container + custom: # is a core.v1.Container image: gcr.io/cloud-builders/git:latest args: [ "clone", "https://...", "other-place" ] @@ -154,8 +161,8 @@ spec: # image either provided as pre-built container, or built by Elafros from # source. When built by elafros, set to the same as build template, e.g. # build.template.arguments[_IMAGE], as the "promise" of a future build. - # If buildName is provided, it is expected that this image will be - # present when the referenced build is complete. + # If buildName is provided, it is expected that this image will be + # present when the referenced build is complete. image: gcr.io/... command: ['run'] args: [] @@ -191,6 +198,9 @@ status: ## Revision +For a high-level description of Revisions, +[see the overview](overview.md#revision). + ```yaml apiVersion: elafros.dev/v1alpha1 kind: Revision @@ -231,11 +241,12 @@ spec: serviceAccountName: ... # Name of the service account the code should run as. ... status: - # This is a copy of metadata from the container image or grafeas, indicating - # the provenance of the revision, from the container + # This is a copy of metadata from the container image or grafeas, + # indicating the provenance of the revision. This is based on the + # container image, but may need further clarification. imageSource: - archive|manifest|repository: ... - conditions: # See also the [error conditions documentation](errors.md) + git|gcs: ... + conditions: # See also the documentation in errors.md - type: Ready status: False message: "Starting Instances"