├── .dependabot └── config.yml ├── .dockerignore ├── .editorconfig ├── .envrc ├── .envsh ├── .gitattributes ├── .gitignore ├── .goversion ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── client ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── Articles.tsx │ ├── Form.tsx │ ├── index.css │ ├── index.tsx │ ├── proto │ │ ├── proto_grpc_web_pb.d.ts │ │ ├── proto_grpc_web_pb.js │ │ ├── proto_pb.d.ts │ │ └── proto_pb.js │ ├── react-app-env.d.ts │ ├── serviceWorker.ts │ └── setupTests.ts └── tsconfig.json ├── envoy.Dockerfile ├── envoy.yaml ├── go.mod ├── go.sum ├── images ├── architecture.png ├── c9after.png ├── c9before.png ├── c9preview.png └── example.gif ├── mesh ├── mesh.json ├── serverNode.json ├── serverRoute.json ├── serverRouter.json └── serverService.json ├── proto ├── proto.pb.go └── proto.proto ├── scripts ├── deploy.sh ├── entrypoint.sh └── resize.sh ├── server.Dockerfile ├── server ├── cmd │ ├── flags.go │ ├── root.go │ ├── run.go │ └── services.go └── main.go └── templates ├── app.yaml ├── infra.yaml └── mesh.yaml /.dependabot/config.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | update_configs: 3 | - package_manager: "javascript" 4 | directory: "/client" 5 | update_schedule: "monthly" 6 | automerged_updates: 7 | - match: 8 | dependency_type: "all" 9 | update_type: "semver:minor" 10 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | client 2 | zk-single-kafka-single 3 | bin 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | 17 | [{Makefile,**.mk}] 18 | # Use tabs for indentation (Makefiles require tabs) 19 | indent_style = tab 20 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SCRIPT=`python -c "import os,sys; print(os.path.realpath(os.path.expanduser(sys.argv[1])))" "${BASH_SOURCE:-$0}"` 3 | export PROJECT_DIR=$(dirname $SCRIPT) 4 | export GO111MODULE=on 5 | export DOCKER_HOST_IP=127.0.0.1 6 | export GOPROXY=direct 7 | 8 | WANT_VERSION=$(cat .goversion) 9 | GOT_VERSION=$(go version | awk '{print $3}') 10 | if [ "$WANT_VERSION" != "$GOT_VERSION" ]; then 11 | echo "!! The example is using $WANT_VERSION, but you're running $GOT_VERSION." 12 | echo "!! Some operations may not work as expected." 13 | fi 14 | -------------------------------------------------------------------------------- /.envsh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SCRIPT=`python -c "import os,sys; print(os.path.realpath(os.path.expanduser(sys.argv[1])))" "${BASH_SOURCE:-$0}"` 3 | export PROJECT_DIR=$(dirname $SCRIPT) 4 | export GO111MODULE=on 5 | export DOCKER_HOST_IP=127.0.0.1 6 | export GOPROXY=direct 7 | 8 | WANT_VERSION=$(cat .goversion) 9 | GOT_VERSION=$(go version | awk '{print $3}') 10 | if [ "$WANT_VERSION" != "$GOT_VERSION" ]; then 11 | echo "!! The example is using $WANT_VERSION, but you're running $GOT_VERSION." 12 | echo "!! Some operations may not work as expected." 13 | fi 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion 3 | * text=auto 4 | 5 | # For the following file types, normalize line endings to LF on 6 | # checkin and prevent conversion to CRLF when they are checked out 7 | # (this is required in order to prevent newline related issues like, 8 | # for example, after the build script is run) 9 | .* text eol=lf 10 | *.css text eol=lf 11 | *.html text eol=lf 12 | *.js text eol=lf 13 | *.json text eol=lf 14 | *.md text eol=lf 15 | *.sh text eol=lf 16 | *.txt text eol=lf 17 | *.xml text eol=lf 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS generated files 2 | .DS_Store 3 | ehthumbs.db 4 | Icon? 5 | Thumbs.db 6 | 7 | bin 8 | release/* 9 | NOTES* 10 | zk-single-kafka-single 11 | -------------------------------------------------------------------------------- /.goversion: -------------------------------------------------------------------------------- 1 | go1.13.8 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | 3 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 4 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 5 | opensource-codeofconduct@amazon.com with any additional questions or comments. 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | ## Reporting Bugs/Feature Requests 10 | 11 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 12 | 13 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 14 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 15 | 16 | - A reproducible test case or series of steps 17 | - The version of our code being used 18 | - Any modifications you've made relevant to the bug 19 | - Anything unusual about your environment or deployment 20 | 21 | ## Contributing via Pull Requests 22 | 23 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 24 | 25 | 1. You are working against the latest source on the _master_ branch. 26 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 27 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 28 | 29 | To send us a pull request, please: 30 | 31 | 1. Fork the repository. 32 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 33 | 3. Ensure local tests pass. 34 | 4. Commit to your fork using clear commit messages. 35 | 5. Send us a pull request, answering any default questions in the pull request interface. 36 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 37 | 38 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 39 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 40 | 41 | ## Finding contributions to work on 42 | 43 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 44 | 45 | ## Code of Conduct 46 | 47 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 48 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 49 | opensource-codeofconduct@amazon.com with any additional questions or comments. 50 | 51 | ## Security issue notifications 52 | 53 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 54 | 55 | ## Licensing 56 | 57 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 58 | 59 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Amazon Web Services, Inc. or its Affiliates 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MODULE = $(shell env GO111MODULE=on $(GO) list -m) 2 | PKGS = $(or $(PKG),$(shell env GO111MODULE=on $(GO) list ./...)) 3 | BIN = $(CURDIR)/bin 4 | DEPLOY = $(CURDIR)/scripts/deploy.sh 5 | RESIZE = $(CURDIR)/scripts/resize.sh 6 | CLIENT = $(CURDIR)/client 7 | 8 | GO = go 9 | TIMEOUT = 15 10 | V = 0 11 | Q = $(if $(filter 1,$V),,@) 12 | M = $(shell printf "\033[34;1m▶\033[0m") 13 | 14 | export GO111MODULE=on 15 | 16 | .PHONY: all 17 | all: fmt lint | $(BIN) ; $(info $(M) building executable…) @ ## Build program binary 18 | $Q $(GO) build \ 19 | -tags release \ 20 | -o $(BIN)/$(basename $(MODULE)) server/main.go 21 | 22 | # Tools 23 | 24 | $(BIN): 25 | @mkdir -p $@ 26 | $(BIN)/%: | $(BIN) ; $(info $(M) building $(PACKAGE)…) 27 | $Q tmp=$$(mktemp -d); \ 28 | env GO111MODULE=off GOPATH=$$tmp GOBIN=$(BIN) $(GO) get $(PACKAGE) \ 29 | || ret=$$?; \ 30 | rm -rf $$tmp ; exit $$ret 31 | 32 | GOLINT = $(BIN)/golint 33 | $(BIN)/golint: PACKAGE=golang.org/x/lint/golint 34 | 35 | # Start 36 | 37 | .PHONY: install 38 | install: ; $(info $(M) installing…) @ ## Start the client 39 | @cd $(CLIENT); yarn -s 40 | 41 | .PHONY: start 42 | start: ; $(info $(M) start…) @ ## Start the client 43 | @if [ ! -d "${CURDIR}/client/node_modules" ]; then make install; fi 44 | @URL="$$($(DEPLOY) print_endpoint)"; (cd client; REACT_APP_ENDPOINT=$$URL yarn start) 45 | 46 | # Deploy 47 | 48 | .PHONY: deploy 49 | deploy: ; $(info $(M) deploy…) @ ## Deploy the application 50 | $Q $(DEPLOY) 51 | 52 | .PHONY: print_endpoint 53 | print_endpoint: ; $(info $(M) print endpoint…) @ ## Print the endpoint 54 | $Q $(DEPLOY) print_endpoint 55 | 56 | .PHONY: delete 57 | delete: ; $(info $(M) delete…) @ ## Delete the application 58 | $Q $(DEPLOY) delete 59 | 60 | # Code 61 | 62 | .PHONY: lint 63 | lint: | $(GOLINT) ; $(info $(M) running golint…) @ ## Run golint 64 | $Q $(GOLINT) -set_exit_status $(PKGS) 65 | 66 | .PHONY: fmt 67 | fmt: ; $(info $(M) running gofmt…) @ ## Run gofmt on all source files 68 | $Q $(GO) fmt $(PKGS) 69 | 70 | # Misc 71 | 72 | .PHONY: proto 73 | proto: ; $(info $(M) generating service ...) @ ## Generating gRPC service 74 | @protoc --go_out=plugins=grpc:./proto --js_out=import_style=commonjs:./client/src/proto --grpc-web_out=import_style=commonjs+dts,mode=grpcwebtext:./client/src/proto --proto_path=proto proto/*.proto 75 | 76 | .PHONY: clean 77 | clean: ; $(info $(M) cleaning…) @ ## Cleanup everything 78 | @rm -rf $(BIN) 79 | @rm -rf $(CLIENT)/node_modules 80 | 81 | .PHONY: resize 82 | resize: ; $(info $(M) resizing…) @ ## Resizing Cloud9 83 | $Q $(RESIZE) 84 | 85 | .PHONY: help 86 | help: 87 | @grep -hE '^[ a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ 88 | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-17s\033[0m %s\n", $$1, $$2}' 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Streaming web content with a log-based architecture with Amazon MSK 2 | 3 | The repository contains the sample code for the [article](https://aws.amazon.com/blogs/big-data/streaming-web-content-with-a-log-based-architecture-with-amazon-msk/) 4 | 5 | ## Introduction 6 | 7 | ![Example](images/example.gif "Example") 8 | 9 | The repository contains the example of a [microblogging](https://en.wikipedia.org/wiki/Microblogging) service that uses a log-based architecture on [Amazon MSK](https://aws.amazon.com/msk/). It consists of a [React](https://reactjs.org/) app that allows you to publish and read articles. A service which implements the [gRPC](https://grpc.io/) service to publish and subscribe to the messages (articles) that are created. And the [AWS CloudFormation](https://aws.amazon.com/cloudformation/) templates to run the service on the AWS cloud. The client uses [gRPC for Web Clients](https://github.com/grpc/grpc-web) to communicate with the pub/sub service. 10 | 11 | ## Technologies 12 | 13 | - [Protobuf](https://developers.google.com/protocol-buffers) protocol for the service 14 | - [React](https://reactjs.org/) app with [gRPC for Web Clients](https://github.com/grpc/grpc-web) 15 | - [gRPC](https://grpc.io) service to subscribe and publish articles 16 | - [AWS CloudFormation](https://aws.amazon.com/cloudformation/) templates to run the service 17 | 18 | ## Architecture 19 | 20 | The architecture for the service is provisioned by two [CloudFormation](https://aws.amazon.com/cloudformation/) stacks. A core stack that contains naive AWS components like VPC, NAT Gateway and Amazon MSK. And a second app stack, which provisions the app on [Fargate](https://aws.amazon.com/fargate/) with an [Application Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html) 21 | 22 | ![Application Architecture](images/architecture.png "Microblogging Service") 23 | 24 | --- 25 | 26 | ## Running the example 27 | 28 | You can either run the example on 29 | 30 | - [AWS Cloud9](#run-on-aws-cloud9) 31 | - [Your machine](#run-on-your-machine). 32 | 33 | > We recommend using [AWS Cloud9](https://aws.amazon.com/cloud9/) to discover the example. 34 | 35 | --- 36 | 37 | ## Run on AWS Cloud9 38 | 39 | First, you need your own AWS account. Follow these [steps](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/) if you do not have an account. 40 | 41 | > :warning: Running the application will create and consume AWS resources. This will cost you money. Make sure you shut down/remove all resources once you are finished to avoid ongoing charges to your AWS account. 42 | 43 | ### Create a Workspace 44 | 45 | > :warning: The [AWS Cloud9](https://aws.amazon.com/cloud9/) workspace should be created by an IAM user with Administrator privileges, not the root account user. Please ensure that you are logged in as an IAM user, not the root account user. 46 | 47 | - [Open AWS Cloud9 in the AWS Console](https://console.aws.amazon.com/cloud9/home). 48 | - Select **Create environment** to create a new workspace 49 | - Name it **mskworkshop**, click _Next step_. 50 | - Choose **"t3.small"** as _Instance Type_, take all default values and click _Next Step_ 51 | - On the overview double check your inputs and click _Create Environment_ 52 | 53 | > :boom: AWS Cloud9 provides a default auto-hibernation setting of 30 minutes for your Amazon EC2 instances created through Cloud9. With this setting, your EC2 instances automatically stop 30 minutes after you close the IDE and restart only when you reopen the IDE. 54 | 55 | - When your workspace is ready, customize the environment by closing the _Welcome_ tab, and opening up a new tab in the workspace. 56 | 57 | ![c9before](/images/c9before.png) 58 | 59 | - Your workspace should look like this now. 60 | 61 | ![c9before](/images/c9after.png) 62 | 63 | > :boom: If you prefer a different theme, you can choose one by selecting _View > Themes > Solarized > Solarized Dark_ 64 | 65 | ### Update to the latest AWS CLI 66 | 67 | - Run the following command to view the current version of the [AWS CLI](https://aws.amazon.com/cli/). 68 | 69 | ```bash 70 | aws --version 71 | ``` 72 | 73 | - Update to the latest version. 74 | 75 | ```bash 76 | pip install --user --upgrade awscli 77 | ``` 78 | 79 | - Confirm that you have a newer version running. 80 | 81 | ```bash 82 | aws --version 83 | ``` 84 | 85 | ### Install tools 86 | 87 | The workshop needs some tools to be installed in the environment. 88 | 89 | ```bash 90 | sudo yum install -y jq 91 | ``` 92 | 93 | This will install Node.js in your Cloud9 environment. 94 | 95 | ```bash 96 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash 97 | ``` 98 | 99 | Active the environment. 100 | 101 | ```bash 102 | . ~/.nvm/nvm.sh 103 | ``` 104 | 105 | Use `nvm` to install a current version of Node.js. 106 | 107 | ```bash 108 | nvm install node 109 | ``` 110 | 111 | Later we will use the [yarn](https://yarnpkg.com/) package manager for installing the client packages. 112 | 113 | ```bash 114 | npm install yarn -g 115 | ``` 116 | 117 | > :warning: There is an extensive [tutorial](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-up-node-on-ec2-instance.html) that walks through the steps of setting up Node.js on an Amazon EC2 instance. 118 | 119 | ### Clone the workshop 120 | 121 | You will need to clone the workshop to your [AWS Cloud9](https://aws.amazon.com/cloud9/) workspace. 122 | 123 | ```bash 124 | # First, be sure you are in your environment directory 125 | cd ~/environment 126 | ``` 127 | 128 | Clone the respository to your environment directory and change into the directory 129 | 130 | ```bash 131 | git clone https://github.com/aws-samples/aws-msk-content-streaming aws-msk-content-streaming && cd $_ 132 | ``` 133 | 134 | ### Resize the environment 135 | 136 | By default Cloud9 has 8GB storage attached. To build the containers you need more space. 137 | 138 | ```bash 139 | make resize 140 | ``` 141 | 142 | This will resize your environment to 20GB storage. 143 | 144 | > If encounter an error that the `/dev/nvme0n1` device does not exists, then this means you are not running on a Nitro-based architecture. Please replace the devices as explained [here](https://docs.aws.amazon.com/cloud9/latest/user-guide/move-environment.html) with the right ones. 145 | 146 | ### Create an SSH Key 147 | 148 | Please run this command to generate SSH Key in Cloud9. This key will be used on the worker node instances to allow ssh access if necessary. 149 | 150 | ```bash 151 | ssh-keygen 152 | ``` 153 | 154 | > :warning: Press `enter` 3 times to take the default choices 155 | 156 | Upload the public key to your EC2 region. 157 | 158 | ```bash 159 | aws ec2 import-key-pair --key-name ${C9_PROJECT} --public-key-material file://~/.ssh/id_rsa.pub 160 | ``` 161 | 162 | If you got an error similar to `An error occurred (InvalidKey.Format) when calling the ImportKeyPair operation: Key is not in valid OpenSSH public key format` then you can try this command instead. 163 | 164 | ```bash 165 | aws ec2 import-key-pair --key-name ${C9_PROJECT} --public-key-material fileb://~/.ssh/id_rsa.pub 166 | ``` 167 | 168 | Set the environment variable for the `KEY_PAIR`. 169 | 170 | ```bash 171 | export KEY_PAIR=${C9_PROJECT} 172 | ``` 173 | 174 | ### Deploy the workshop 175 | 176 | Running the script will deploy the application. 177 | 178 | ```bash 179 | make deploy 180 | ``` 181 | 182 | > This can take a while, as you will create a high available Kafka with Amazon MSK. 183 | 184 | > :warning: The deploy scripts detect when you are running the deploy in a [AWS Cloud9](https://aws.amazon.com/cloud9/) workspace. It sets the `PROJECT_NAME` to your Cloud9 environment name, and extracts the `AWS_ACCOUNT_ID` and `AWS_DEFAULT_REGION`. You can override any of these variables 185 | 186 | When the process is finished, you can start the React app. It will start the application with the `REACT_APP_ENDPOINT` environment variable which is set to the URL of the provisioned Application Load Balancer. 187 | 188 | ```bash 189 | make start 190 | ``` 191 | 192 | When the application is finished to be installed you will see a message that it `Compiled successfully!`. You can access the preview by selecting _Preview > Preview Running Runnin Application_ from the toolbar. This will open a new tab with the application. 193 | 194 | :warning: you cannot post new content yet. Because we do not have a custom domain, we have not enabled HTTPS with our service. You will have to access the preview URL with HTTP. 195 | 196 | Either copy the full url (e.g. `https://12345678910.vfs.cloud9.eu-west-1.amazonaws.com/`) and replace `https` with `http`. Or click on the _Pop Out Into New Window_ button next to the browser bar and then replace it. 197 | 198 | ![c9preview](/images/c9preview.png) 199 | 200 | You can now test it by creating an new item. Give it a title and add some content. If you have finished click _Create Post_. 201 | 202 | > You can access the app at [localhost:3000](http://localhost:3000) if you run the example on your local machine. 203 | 204 | ### Cleanup 205 | 206 | The last step is to cleanup your account. This will delete all created resources. 207 | 208 | ```bash 209 | make delete 210 | ``` 211 | 212 | ## Run on your machine 213 | 214 | You can either run the example on your own machine, or [run on AWS Cloud9](#run-on-aws-cloud9). If you run it on your own machine you will have to install some additional tools. 215 | 216 | - [Docker](https://docs.docker.com/install/) 217 | - [AWS Command Line Interface](https://aws.amazon.com/cli/) 218 | - [Node.js](https://nodejs.org/en/) and [Yarn Package Manager](https://yarnpkg.com/) 219 | - Linux userland with bash 220 | - [GNU Make](https://www.gnu.org/software/make/) 221 | 222 | You will have to clone the repository 223 | 224 | ```bash 225 | git clone https://github.com/aws-samples/aws-msk-content-streaming aws-msk-content-streaming && cd $_ 226 | ``` 227 | 228 | and manually set the environment variables for the project. 229 | 230 | ```bash 231 | export PROJECT_NAME= 232 | export AWS_ACCOUNT_ID= 233 | export AWS_DEFAULT_REGION= 234 | ``` 235 | 236 | You can then deploy the CloudFormation Stacks via. 237 | 238 | ```bash 239 | make deploy 240 | ``` 241 | 242 | Start the development server at [localhost:3000](http://localhost:3000) if you run the example on your local machine. 243 | 244 | ```bash 245 | make start 246 | ``` 247 | 248 | And jump to the [cleanup](#cleanup) if you have finished playing around with the example. 249 | 250 | ## License 251 | 252 | [MIT](/LICENSE) 253 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `yarn eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@improbable-eng/grpc-web": "^0.12.0", 7 | "@testing-library/jest-dom": "^5.11.0", 8 | "@testing-library/react": "^10.4.3", 9 | "@testing-library/user-event": "^12.0.11", 10 | "@types/jest": "^26.0.3", 11 | "@types/node": "^14.0.14", 12 | "@types/react": "^16.9.0", 13 | "@types/react-dom": "^16.9.0", 14 | "evergreen-ui": "^4.23.0", 15 | "formik": "^2.1.4", 16 | "google-protobuf": "^3.11.4", 17 | "grpc-web": "^1.0.7", 18 | "husky": "^4.2.3", 19 | "lint-staged": "^10.0.8", 20 | "prettier": "^2.0.5", 21 | "react": "^16.13.0", 22 | "react-dom": "^16.13.0", 23 | "react-hook-form": "^6.0.0", 24 | "react-scripts": "3.4.1", 25 | "ts-protoc-gen": "^0.12.1-pre.a78a914", 26 | "typescript": "~3.9.6" 27 | }, 28 | "scripts": { 29 | "start": "EXTEND_ESLINT=true react-scripts start", 30 | "build": "EXTEND_ESLINT=true react-scripts build", 31 | "test": "react-scripts test", 32 | "eject": "react-scripts eject", 33 | "proto": "protoc --plugin=\"protoc-gen-ts=./node_modules/.bin/protoc-gen-ts\" --js_out=\"import_style=commonjs,binary:./src/proto\" --ts_out=\"service=grpc-web:./src/proto\" --proto_path=../proto ../proto/*.proto" 34 | }, 35 | "eslintConfig": { 36 | "extends": "react-app", 37 | "overrides": [ 38 | { 39 | "files": "**/*.js", 40 | "excludedFiles": "**/*_pb.js" 41 | } 42 | ] 43 | }, 44 | "browserslist": { 45 | "production": [ 46 | ">0.2%", 47 | "not dead", 48 | "not op_mini all" 49 | ], 50 | "development": [ 51 | "last 1 chrome version", 52 | "last 1 firefox version", 53 | "last 1 safari version" 54 | ] 55 | }, 56 | "lint-staged": { 57 | "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [ 58 | "prettier --write" 59 | ] 60 | }, 61 | "husky": { 62 | "hooks": { 63 | "pre-commit": "lint-staged" 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-msk-content-streaming/a78fdfdcd625d6640e66c3c998aa4dcf7a8efe88/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-msk-content-streaming/a78fdfdcd625d6640e66c3c998aa4dcf7a8efe88/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-msk-content-streaming/a78fdfdcd625d6640e66c3c998aa4dcf7a8efe88/client/public/logo512.png -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /client/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { ListArticles, Article } from "./proto/proto_pb"; 3 | import { MicroClient } from "./proto/proto_grpc_web_pb"; 4 | import { Pane } from "evergreen-ui"; 5 | import { toaster } from "evergreen-ui"; 6 | import ArticlesList from "./Articles"; 7 | import InsertForm from "./Form"; 8 | import React, { useState, useEffect } from "react"; 9 | 10 | declare var process: { 11 | env: { 12 | REACT_APP_ENDPOINT: string; 13 | }; 14 | }; 15 | 16 | const mono = new MicroClient(process.env.REACT_APP_ENDPOINT); 17 | 18 | function App() { 19 | // save articles state, this is the list of provided articles 20 | const [articles, setArticles] = useState>([]); 21 | 22 | const addArticles = (newArticles: Array
) => 23 | setArticles(state => [...state, ...newArticles]); 24 | 25 | // the subscription is a side-effect for the rendering, 26 | // we want to avoid to do a new request with every render 27 | useEffect(() => { 28 | const req = new ListArticles.Request(); 29 | const resp = mono.listArticles(req); 30 | 31 | resp.on("data", (resp: ListArticles.Response) => { 32 | const list = resp.getArticlesList(); 33 | addArticles(list); 34 | }); 35 | 36 | resp.on("error", err => { 37 | toaster.danger("Argh! Please reload the browser.", { 38 | duration: 60 * 60, 39 | hasCloseButton: true 40 | }); 41 | }); 42 | 43 | resp.on("status", status => { 44 | toaster.notify(`The client status changed to ${status}.`); 45 | }); 46 | 47 | resp.on("end", function() { 48 | toaster.notify("Argh! The client has disconnected."); 49 | }); 50 | }, []); 51 | 52 | return ( 53 |
54 | 61 | 62 | 63 | 64 | 65 | 66 |
67 | ); 68 | } 69 | 70 | export default App; 71 | -------------------------------------------------------------------------------- /client/src/Articles.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Pane, Card, Paragraph, Heading } from "evergreen-ui"; 3 | import { Article } from "./proto/proto_pb"; 4 | 5 | export default function(props: any) { 6 | // deconstructing articles into a list and unwrap into aticle 7 | const articles = props.articles; 8 | const listArticles = articles.map((article: Article) => ( 9 | 10 | {article.getTitle()} 11 | {article.getBody()} 12 | 13 | )); 14 | 15 | // render a pane with the articles 16 | return {articles.length > 0 ? listArticles : "No items"}; 17 | } 18 | -------------------------------------------------------------------------------- /client/src/Form.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useFormik } from "formik"; 3 | import { MicroClient } from "./proto/proto_grpc_web_pb"; 4 | import { Article, Insert, Item } from "./proto/proto_pb"; 5 | import { Button, TextInputField, InlineAlert, toaster } from "evergreen-ui"; 6 | 7 | const defaultFields = { 8 | body: "", 9 | title: "" 10 | }; 11 | 12 | type Props = { client: MicroClient }; 13 | type Fields = { body: string; title: string } & typeof defaultFields; 14 | interface Errors { 15 | title?: string; 16 | body?: string; 17 | } 18 | 19 | // A custom validation function. This must return an object 20 | // which keys are symmetrical to our values/initialValues 21 | const validate = (values: Fields) => { 22 | const errors: Errors = {}; 23 | 24 | if (!values.title) { 25 | errors.title = "You have to give your post a nice title"; 26 | } 27 | 28 | if (!values.body) { 29 | errors.body = "You have to provide some content"; 30 | } 31 | 32 | return errors; 33 | }; 34 | 35 | const InsertForm = ({ client }: Props) => { 36 | const formik = useFormik({ 37 | initialValues: { 38 | body: "", 39 | title: "" 40 | }, 41 | validate, 42 | onSubmit: values => { 43 | const req = new Insert.Request(); 44 | 45 | const article = new Article(); 46 | article.setBody(values.body); 47 | article.setTitle(values.title); 48 | 49 | const item = new Item(); 50 | item.setArticle(article); 51 | req.setItem(item); 52 | 53 | client.insert(req, {}, (err, _) => { 54 | if (err) { 55 | toaster.danger(`Upps! Could not publish your article.`); 56 | 57 | return; 58 | } 59 | 60 | toaster.success("Yeah! Your article was published."); 61 | }); 62 | } 63 | }); 64 | 65 | return ( 66 |
67 | 73 | {formik.errors.title ? ( 74 | 75 | {formik.errors.title} 76 | 77 | ) : null} 78 | 84 | {formik.errors.body ? ( 85 | 86 | {formik.errors.body} 87 | 88 | ) : null} 89 | 90 | 91 | ); 92 | }; 93 | 94 | export default InsertForm; 95 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /client/src/proto/proto_grpc_web_pb.d.ts: -------------------------------------------------------------------------------- 1 | import * as grpcWeb from "grpc-web"; 2 | 3 | import * as google_protobuf_timestamp_pb from "google-protobuf/google/protobuf/timestamp_pb"; 4 | 5 | import { 6 | Request, 7 | Response, 8 | Request, 9 | Response, 10 | Request, 11 | Response 12 | } from "./proto_pb"; 13 | 14 | export class MicroClient { 15 | constructor( 16 | hostname: string, 17 | credentials?: null | { [index: string]: string }, 18 | options?: null | { [index: string]: string } 19 | ); 20 | 21 | insert( 22 | request: Insert.Request, 23 | metadata: grpcWeb.Metadata | undefined, 24 | callback: (err: grpcWeb.Error, response: Insert.Response) => void 25 | ): grpcWeb.ClientReadableStream; 26 | 27 | update( 28 | request: Update.Request, 29 | metadata: grpcWeb.Metadata | undefined, 30 | callback: (err: grpcWeb.Error, response: Update.Response) => void 31 | ): grpcWeb.ClientReadableStream; 32 | 33 | listArticles( 34 | request: ListArticles.Request, 35 | metadata?: grpcWeb.Metadata 36 | ): grpcWeb.ClientReadableStream; 37 | } 38 | 39 | export class MicroPromiseClient { 40 | constructor( 41 | hostname: string, 42 | credentials?: null | { [index: string]: string }, 43 | options?: null | { [index: string]: string } 44 | ); 45 | 46 | insert( 47 | request: Insert.Request, 48 | metadata?: grpcWeb.Metadata 49 | ): Promise; 50 | 51 | update( 52 | request: Update.Request, 53 | metadata?: grpcWeb.Metadata 54 | ): Promise; 55 | 56 | listArticles( 57 | request: ListArticles.Request, 58 | metadata?: grpcWeb.Metadata 59 | ): grpcWeb.ClientReadableStream; 60 | } 61 | -------------------------------------------------------------------------------- /client/src/proto/proto_grpc_web_pb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview gRPC-Web generated client stub for proto 3 | * @enhanceable 4 | * @public 5 | */ 6 | 7 | // GENERATED CODE -- DO NOT EDIT! 8 | /* eslint-disable */ 9 | 10 | const grpc = {}; 11 | grpc.web = require("grpc-web"); 12 | 13 | var google_protobuf_timestamp_pb = require("google-protobuf/google/protobuf/timestamp_pb.js"); 14 | const proto = {}; 15 | proto.proto = require("./proto_pb.js"); 16 | 17 | /** 18 | * @param {string} hostname 19 | * @param {?Object} credentials 20 | * @param {?Object} options 21 | * @constructor 22 | * @struct 23 | * @final 24 | */ 25 | proto.proto.MicroClient = function(hostname, credentials, options) { 26 | if (!options) options = {}; 27 | options["format"] = "text"; 28 | 29 | /** 30 | * @private @const {!grpc.web.GrpcWebClientBase} The client 31 | */ 32 | this.client_ = new grpc.web.GrpcWebClientBase(options); 33 | 34 | /** 35 | * @private @const {string} The hostname 36 | */ 37 | this.hostname_ = hostname; 38 | }; 39 | 40 | /** 41 | * @param {string} hostname 42 | * @param {?Object} credentials 43 | * @param {?Object} options 44 | * @constructor 45 | * @struct 46 | * @final 47 | */ 48 | proto.proto.MicroPromiseClient = function(hostname, credentials, options) { 49 | if (!options) options = {}; 50 | options["format"] = "text"; 51 | 52 | /** 53 | * @private @const {!grpc.web.GrpcWebClientBase} The client 54 | */ 55 | this.client_ = new grpc.web.GrpcWebClientBase(options); 56 | 57 | /** 58 | * @private @const {string} The hostname 59 | */ 60 | this.hostname_ = hostname; 61 | }; 62 | 63 | /** 64 | * @const 65 | * @type {!grpc.web.MethodDescriptor< 66 | * !proto.proto.Insert.Request, 67 | * !proto.proto.Insert.Response>} 68 | */ 69 | const methodDescriptor_Micro_Insert = new grpc.web.MethodDescriptor( 70 | "/proto.Micro/Insert", 71 | grpc.web.MethodType.UNARY, 72 | proto.proto.Insert.Request, 73 | proto.proto.Insert.Response, 74 | /** 75 | * @param {!proto.proto.Insert.Request} request 76 | * @return {!Uint8Array} 77 | */ 78 | function(request) { 79 | return request.serializeBinary(); 80 | }, 81 | proto.proto.Insert.Response.deserializeBinary 82 | ); 83 | 84 | /** 85 | * @const 86 | * @type {!grpc.web.AbstractClientBase.MethodInfo< 87 | * !proto.proto.Insert.Request, 88 | * !proto.proto.Insert.Response>} 89 | */ 90 | const methodInfo_Micro_Insert = new grpc.web.AbstractClientBase.MethodInfo( 91 | proto.proto.Insert.Response, 92 | /** 93 | * @param {!proto.proto.Insert.Request} request 94 | * @return {!Uint8Array} 95 | */ 96 | function(request) { 97 | return request.serializeBinary(); 98 | }, 99 | proto.proto.Insert.Response.deserializeBinary 100 | ); 101 | 102 | /** 103 | * @param {!proto.proto.Insert.Request} request The 104 | * request proto 105 | * @param {?Object} metadata User defined 106 | * call metadata 107 | * @param {function(?grpc.web.Error, ?proto.proto.Insert.Response)} 108 | * callback The callback function(error, response) 109 | * @return {!grpc.web.ClientReadableStream|undefined} 110 | * The XHR Node Readable Stream 111 | */ 112 | proto.proto.MicroClient.prototype.insert = function( 113 | request, 114 | metadata, 115 | callback 116 | ) { 117 | return this.client_.rpcCall( 118 | this.hostname_ + "/proto.Micro/Insert", 119 | request, 120 | metadata || {}, 121 | methodDescriptor_Micro_Insert, 122 | callback 123 | ); 124 | }; 125 | 126 | /** 127 | * @param {!proto.proto.Insert.Request} request The 128 | * request proto 129 | * @param {?Object} metadata User defined 130 | * call metadata 131 | * @return {!Promise} 132 | * A native promise that resolves to the response 133 | */ 134 | proto.proto.MicroPromiseClient.prototype.insert = function(request, metadata) { 135 | return this.client_.unaryCall( 136 | this.hostname_ + "/proto.Micro/Insert", 137 | request, 138 | metadata || {}, 139 | methodDescriptor_Micro_Insert 140 | ); 141 | }; 142 | 143 | /** 144 | * @const 145 | * @type {!grpc.web.MethodDescriptor< 146 | * !proto.proto.Update.Request, 147 | * !proto.proto.Update.Response>} 148 | */ 149 | const methodDescriptor_Micro_Update = new grpc.web.MethodDescriptor( 150 | "/proto.Micro/Update", 151 | grpc.web.MethodType.UNARY, 152 | proto.proto.Update.Request, 153 | proto.proto.Update.Response, 154 | /** 155 | * @param {!proto.proto.Update.Request} request 156 | * @return {!Uint8Array} 157 | */ 158 | function(request) { 159 | return request.serializeBinary(); 160 | }, 161 | proto.proto.Update.Response.deserializeBinary 162 | ); 163 | 164 | /** 165 | * @const 166 | * @type {!grpc.web.AbstractClientBase.MethodInfo< 167 | * !proto.proto.Update.Request, 168 | * !proto.proto.Update.Response>} 169 | */ 170 | const methodInfo_Micro_Update = new grpc.web.AbstractClientBase.MethodInfo( 171 | proto.proto.Update.Response, 172 | /** 173 | * @param {!proto.proto.Update.Request} request 174 | * @return {!Uint8Array} 175 | */ 176 | function(request) { 177 | return request.serializeBinary(); 178 | }, 179 | proto.proto.Update.Response.deserializeBinary 180 | ); 181 | 182 | /** 183 | * @param {!proto.proto.Update.Request} request The 184 | * request proto 185 | * @param {?Object} metadata User defined 186 | * call metadata 187 | * @param {function(?grpc.web.Error, ?proto.proto.Update.Response)} 188 | * callback The callback function(error, response) 189 | * @return {!grpc.web.ClientReadableStream|undefined} 190 | * The XHR Node Readable Stream 191 | */ 192 | proto.proto.MicroClient.prototype.update = function( 193 | request, 194 | metadata, 195 | callback 196 | ) { 197 | return this.client_.rpcCall( 198 | this.hostname_ + "/proto.Micro/Update", 199 | request, 200 | metadata || {}, 201 | methodDescriptor_Micro_Update, 202 | callback 203 | ); 204 | }; 205 | 206 | /** 207 | * @param {!proto.proto.Update.Request} request The 208 | * request proto 209 | * @param {?Object} metadata User defined 210 | * call metadata 211 | * @return {!Promise} 212 | * A native promise that resolves to the response 213 | */ 214 | proto.proto.MicroPromiseClient.prototype.update = function(request, metadata) { 215 | return this.client_.unaryCall( 216 | this.hostname_ + "/proto.Micro/Update", 217 | request, 218 | metadata || {}, 219 | methodDescriptor_Micro_Update 220 | ); 221 | }; 222 | 223 | /** 224 | * @const 225 | * @type {!grpc.web.MethodDescriptor< 226 | * !proto.proto.ListArticles.Request, 227 | * !proto.proto.ListArticles.Response>} 228 | */ 229 | const methodDescriptor_Micro_ListArticles = new grpc.web.MethodDescriptor( 230 | "/proto.Micro/ListArticles", 231 | grpc.web.MethodType.SERVER_STREAMING, 232 | proto.proto.ListArticles.Request, 233 | proto.proto.ListArticles.Response, 234 | /** 235 | * @param {!proto.proto.ListArticles.Request} request 236 | * @return {!Uint8Array} 237 | */ 238 | function(request) { 239 | return request.serializeBinary(); 240 | }, 241 | proto.proto.ListArticles.Response.deserializeBinary 242 | ); 243 | 244 | /** 245 | * @const 246 | * @type {!grpc.web.AbstractClientBase.MethodInfo< 247 | * !proto.proto.ListArticles.Request, 248 | * !proto.proto.ListArticles.Response>} 249 | */ 250 | const methodInfo_Micro_ListArticles = new grpc.web.AbstractClientBase.MethodInfo( 251 | proto.proto.ListArticles.Response, 252 | /** 253 | * @param {!proto.proto.ListArticles.Request} request 254 | * @return {!Uint8Array} 255 | */ 256 | function(request) { 257 | return request.serializeBinary(); 258 | }, 259 | proto.proto.ListArticles.Response.deserializeBinary 260 | ); 261 | 262 | /** 263 | * @param {!proto.proto.ListArticles.Request} request The request proto 264 | * @param {?Object} metadata User defined 265 | * call metadata 266 | * @return {!grpc.web.ClientReadableStream} 267 | * The XHR Node Readable Stream 268 | */ 269 | proto.proto.MicroClient.prototype.listArticles = function(request, metadata) { 270 | return this.client_.serverStreaming( 271 | this.hostname_ + "/proto.Micro/ListArticles", 272 | request, 273 | metadata || {}, 274 | methodDescriptor_Micro_ListArticles 275 | ); 276 | }; 277 | 278 | /** 279 | * @param {!proto.proto.ListArticles.Request} request The request proto 280 | * @param {?Object} metadata User defined 281 | * call metadata 282 | * @return {!grpc.web.ClientReadableStream} 283 | * The XHR Node Readable Stream 284 | */ 285 | proto.proto.MicroPromiseClient.prototype.listArticles = function( 286 | request, 287 | metadata 288 | ) { 289 | return this.client_.serverStreaming( 290 | this.hostname_ + "/proto.Micro/ListArticles", 291 | request, 292 | metadata || {}, 293 | methodDescriptor_Micro_ListArticles 294 | ); 295 | }; 296 | 297 | module.exports = proto.proto; 298 | -------------------------------------------------------------------------------- /client/src/proto/proto_pb.d.ts: -------------------------------------------------------------------------------- 1 | import * as jspb from "google-protobuf"; 2 | 3 | import * as google_protobuf_timestamp_pb from "google-protobuf/google/protobuf/timestamp_pb"; 4 | 5 | export class Item extends jspb.Message { 6 | getArticle(): Article | undefined; 7 | setArticle(value?: Article): void; 8 | hasArticle(): boolean; 9 | clearArticle(): void; 10 | 11 | getItemCase(): Item.ItemCase; 12 | 13 | serializeBinary(): Uint8Array; 14 | toObject(includeInstance?: boolean): Item.AsObject; 15 | static toObject(includeInstance: boolean, msg: Item): Item.AsObject; 16 | static serializeBinaryToWriter( 17 | message: Item, 18 | writer: jspb.BinaryWriter 19 | ): void; 20 | static deserializeBinary(bytes: Uint8Array): Item; 21 | static deserializeBinaryFromReader( 22 | message: Item, 23 | reader: jspb.BinaryReader 24 | ): Item; 25 | } 26 | 27 | export namespace Item { 28 | export type AsObject = { 29 | article?: Article.AsObject; 30 | }; 31 | 32 | export enum ItemCase { 33 | ITEM_NOT_SET = 0, 34 | ARTICLE = 10 35 | } 36 | } 37 | 38 | export class Article extends jspb.Message { 39 | getUuid(): string; 40 | setUuid(value: string): void; 41 | 42 | getTitle(): string; 43 | setTitle(value: string): void; 44 | 45 | getBody(): string; 46 | setBody(value: string): void; 47 | 48 | serializeBinary(): Uint8Array; 49 | toObject(includeInstance?: boolean): Article.AsObject; 50 | static toObject(includeInstance: boolean, msg: Article): Article.AsObject; 51 | static serializeBinaryToWriter( 52 | message: Article, 53 | writer: jspb.BinaryWriter 54 | ): void; 55 | static deserializeBinary(bytes: Uint8Array): Article; 56 | static deserializeBinaryFromReader( 57 | message: Article, 58 | reader: jspb.BinaryReader 59 | ): Article; 60 | } 61 | 62 | export namespace Article { 63 | export type AsObject = { 64 | uuid: string; 65 | title: string; 66 | body: string; 67 | }; 68 | } 69 | 70 | export class Insert extends jspb.Message { 71 | serializeBinary(): Uint8Array; 72 | toObject(includeInstance?: boolean): Insert.AsObject; 73 | static toObject(includeInstance: boolean, msg: Insert): Insert.AsObject; 74 | static serializeBinaryToWriter( 75 | message: Insert, 76 | writer: jspb.BinaryWriter 77 | ): void; 78 | static deserializeBinary(bytes: Uint8Array): Insert; 79 | static deserializeBinaryFromReader( 80 | message: Insert, 81 | reader: jspb.BinaryReader 82 | ): Insert; 83 | } 84 | 85 | export namespace Insert { 86 | export type AsObject = {}; 87 | 88 | export class Request extends jspb.Message { 89 | getItem(): Item | undefined; 90 | setItem(value?: Item): void; 91 | hasItem(): boolean; 92 | clearItem(): void; 93 | 94 | serializeBinary(): Uint8Array; 95 | toObject(includeInstance?: boolean): Request.AsObject; 96 | static toObject(includeInstance: boolean, msg: Request): Request.AsObject; 97 | static serializeBinaryToWriter( 98 | message: Request, 99 | writer: jspb.BinaryWriter 100 | ): void; 101 | static deserializeBinary(bytes: Uint8Array): Request; 102 | static deserializeBinaryFromReader( 103 | message: Request, 104 | reader: jspb.BinaryReader 105 | ): Request; 106 | } 107 | 108 | export namespace Request { 109 | export type AsObject = { 110 | item?: Item.AsObject; 111 | }; 112 | } 113 | 114 | export class Response extends jspb.Message { 115 | getUuid(): string; 116 | setUuid(value: string): void; 117 | 118 | serializeBinary(): Uint8Array; 119 | toObject(includeInstance?: boolean): Response.AsObject; 120 | static toObject(includeInstance: boolean, msg: Response): Response.AsObject; 121 | static serializeBinaryToWriter( 122 | message: Response, 123 | writer: jspb.BinaryWriter 124 | ): void; 125 | static deserializeBinary(bytes: Uint8Array): Response; 126 | static deserializeBinaryFromReader( 127 | message: Response, 128 | reader: jspb.BinaryReader 129 | ): Response; 130 | } 131 | 132 | export namespace Response { 133 | export type AsObject = { 134 | uuid: string; 135 | }; 136 | } 137 | } 138 | 139 | export class Update extends jspb.Message { 140 | serializeBinary(): Uint8Array; 141 | toObject(includeInstance?: boolean): Update.AsObject; 142 | static toObject(includeInstance: boolean, msg: Update): Update.AsObject; 143 | static serializeBinaryToWriter( 144 | message: Update, 145 | writer: jspb.BinaryWriter 146 | ): void; 147 | static deserializeBinary(bytes: Uint8Array): Update; 148 | static deserializeBinaryFromReader( 149 | message: Update, 150 | reader: jspb.BinaryReader 151 | ): Update; 152 | } 153 | 154 | export namespace Update { 155 | export type AsObject = {}; 156 | 157 | export class Request extends jspb.Message { 158 | getItem(): Item | undefined; 159 | setItem(value?: Item): void; 160 | hasItem(): boolean; 161 | clearItem(): void; 162 | 163 | serializeBinary(): Uint8Array; 164 | toObject(includeInstance?: boolean): Request.AsObject; 165 | static toObject(includeInstance: boolean, msg: Request): Request.AsObject; 166 | static serializeBinaryToWriter( 167 | message: Request, 168 | writer: jspb.BinaryWriter 169 | ): void; 170 | static deserializeBinary(bytes: Uint8Array): Request; 171 | static deserializeBinaryFromReader( 172 | message: Request, 173 | reader: jspb.BinaryReader 174 | ): Request; 175 | } 176 | 177 | export namespace Request { 178 | export type AsObject = { 179 | item?: Item.AsObject; 180 | }; 181 | } 182 | 183 | export class Response extends jspb.Message { 184 | getUuid(): string; 185 | setUuid(value: string): void; 186 | 187 | serializeBinary(): Uint8Array; 188 | toObject(includeInstance?: boolean): Response.AsObject; 189 | static toObject(includeInstance: boolean, msg: Response): Response.AsObject; 190 | static serializeBinaryToWriter( 191 | message: Response, 192 | writer: jspb.BinaryWriter 193 | ): void; 194 | static deserializeBinary(bytes: Uint8Array): Response; 195 | static deserializeBinaryFromReader( 196 | message: Response, 197 | reader: jspb.BinaryReader 198 | ): Response; 199 | } 200 | 201 | export namespace Response { 202 | export type AsObject = { 203 | uuid: string; 204 | }; 205 | } 206 | } 207 | 208 | export class ListArticles extends jspb.Message { 209 | serializeBinary(): Uint8Array; 210 | toObject(includeInstance?: boolean): ListArticles.AsObject; 211 | static toObject( 212 | includeInstance: boolean, 213 | msg: ListArticles 214 | ): ListArticles.AsObject; 215 | static serializeBinaryToWriter( 216 | message: ListArticles, 217 | writer: jspb.BinaryWriter 218 | ): void; 219 | static deserializeBinary(bytes: Uint8Array): ListArticles; 220 | static deserializeBinaryFromReader( 221 | message: ListArticles, 222 | reader: jspb.BinaryReader 223 | ): ListArticles; 224 | } 225 | 226 | export namespace ListArticles { 227 | export type AsObject = {}; 228 | 229 | export class Request extends jspb.Message { 230 | serializeBinary(): Uint8Array; 231 | toObject(includeInstance?: boolean): Request.AsObject; 232 | static toObject(includeInstance: boolean, msg: Request): Request.AsObject; 233 | static serializeBinaryToWriter( 234 | message: Request, 235 | writer: jspb.BinaryWriter 236 | ): void; 237 | static deserializeBinary(bytes: Uint8Array): Request; 238 | static deserializeBinaryFromReader( 239 | message: Request, 240 | reader: jspb.BinaryReader 241 | ): Request; 242 | } 243 | 244 | export namespace Request { 245 | export type AsObject = {}; 246 | } 247 | 248 | export class Response extends jspb.Message { 249 | getArticlesList(): Array
; 250 | setArticlesList(value: Array
): void; 251 | clearArticlesList(): void; 252 | addArticles(value?: Article, index?: number): Article; 253 | 254 | serializeBinary(): Uint8Array; 255 | toObject(includeInstance?: boolean): Response.AsObject; 256 | static toObject(includeInstance: boolean, msg: Response): Response.AsObject; 257 | static serializeBinaryToWriter( 258 | message: Response, 259 | writer: jspb.BinaryWriter 260 | ): void; 261 | static deserializeBinary(bytes: Uint8Array): Response; 262 | static deserializeBinaryFromReader( 263 | message: Response, 264 | reader: jspb.BinaryReader 265 | ): Response; 266 | } 267 | 268 | export namespace Response { 269 | export type AsObject = { 270 | articlesList: Array; 271 | }; 272 | } 273 | } 274 | 275 | export class Empty extends jspb.Message { 276 | serializeBinary(): Uint8Array; 277 | toObject(includeInstance?: boolean): Empty.AsObject; 278 | static toObject(includeInstance: boolean, msg: Empty): Empty.AsObject; 279 | static serializeBinaryToWriter( 280 | message: Empty, 281 | writer: jspb.BinaryWriter 282 | ): void; 283 | static deserializeBinary(bytes: Uint8Array): Empty; 284 | static deserializeBinaryFromReader( 285 | message: Empty, 286 | reader: jspb.BinaryReader 287 | ): Empty; 288 | } 289 | 290 | export namespace Empty { 291 | export type AsObject = {}; 292 | } 293 | -------------------------------------------------------------------------------- /client/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /client/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | process.env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl, { 112 | headers: { 'Service-Worker': 'script' } 113 | }) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready 142 | .then(registration => { 143 | registration.unregister(); 144 | }) 145 | .catch(error => { 146 | console.error(error.message); 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /client/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /envoy.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM envoyproxy/envoy-dev:latest 2 | 3 | RUN apt-get update && apt-get install -y curl \ 4 | && rm -rf /var/lib/apt/lists/* 5 | 6 | COPY ./envoy.yaml /etc/envoy/envoy.yaml 7 | CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml -l trace --log-path /tmp/envoy_info.log 8 | -------------------------------------------------------------------------------- /envoy.yaml: -------------------------------------------------------------------------------- 1 | static_resources: 2 | listeners: 3 | - name: listener_0 4 | address: 5 | socket_address: { address: 0.0.0.0, port_value: 8080 } 6 | filter_chains: 7 | - filters: 8 | - name: envoy.http_connection_manager 9 | config: 10 | access_log: 11 | - name: envoy.file_access_log 12 | config: 13 | path: "/dev/stdout" 14 | codec_type: auto 15 | stat_prefix: ingress_http 16 | stream_idle_timeout: 0s 17 | route_config: 18 | name: local_route 19 | virtual_hosts: 20 | - name: local_service 21 | domains: ["*"] 22 | routes: 23 | - match: { prefix: "/" } 24 | route: 25 | cluster: digest_service 26 | max_grpc_timeout: 0s 27 | timeout: 0s 28 | cors: 29 | allow_origin_string_match: 30 | - prefix: "*" 31 | allow_methods: GET, PUT, DELETE, POST, OPTIONS 32 | allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout 33 | max_age: "1728000" 34 | expose_headers: custom-header-1,grpc-status,grpc-message 35 | http_filters: 36 | - name: envoy.grpc_web 37 | - name: envoy.cors 38 | - name: envoy.router 39 | clusters: 40 | - name: digest_service 41 | connect_timeout: 0.25s 42 | type: logical_dns 43 | http2_protocol_options: {} 44 | lb_policy: round_robin 45 | hosts: 46 | [ 47 | { 48 | socket_address: { address: server.content.local, port_value: 9090 }, 49 | }, 50 | ] 51 | upstream_connection_options: 52 | tcp_keepalive: 53 | keepalive_probes: 1 54 | keepalive_time: 10 55 | keepalive_interval: 10 56 | 57 | admin: 58 | access_log_path: "/dev/null" 59 | address: 60 | socket_address: 61 | address: 0.0.0.0 62 | port_value: 8001 63 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/katallaxie/content_streaming_msk 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/Shopify/sarama v1.26.1 7 | github.com/andersnormal/pkg v0.0.0-20190904210201-9dfdf11cc13f 8 | github.com/golang/protobuf v1.3.4 9 | github.com/google/uuid v1.1.1 10 | github.com/sirupsen/logrus v1.4.2 11 | github.com/spf13/cobra v0.0.6 12 | github.com/spf13/viper v1.6.2 13 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b // indirect 14 | google.golang.org/grpc v1.27.1 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 5 | github.com/Shopify/sarama v1.26.1 h1:3jnfWKD7gVwbB1KSy/lE0szA9duPuSFLViK0o/d3DgA= 6 | github.com/Shopify/sarama v1.26.1/go.mod h1:NbSGBSSndYaIhRcBtY9V0U7AyH+x71bG668AuWys/yU= 7 | github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= 8 | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= 9 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 10 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 11 | github.com/andersnormal/pkg v0.0.0-20190904210201-9dfdf11cc13f h1:At9OTg1vHqIwDy3IF5d4gdbBvLUjAoHvK97miJNs5NY= 12 | github.com/andersnormal/pkg v0.0.0-20190904210201-9dfdf11cc13f/go.mod h1:BXOm+9TvJq2Ikvsu+ZZkERYaEgGOAf22HKRGG1ztNxs= 13 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 14 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 15 | github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= 16 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 17 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 18 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 19 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 20 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 21 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 22 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 23 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 24 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 25 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 26 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 28 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 29 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 30 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 31 | github.com/eapache/go-resiliency v1.2.0 h1:v7g92e/KSN71Rq7vSThKaWIq68fL4YHvWyiUKorFR1Q= 32 | github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= 33 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw= 34 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= 35 | github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= 36 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 37 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 38 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 39 | github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= 40 | github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= 41 | github.com/frankban/quicktest v1.7.2 h1:2QxQoC1TS09S7fhCPsrvqYdvP1H5M1P1ih5ABm3BTYk= 42 | github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= 43 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 44 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 45 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 46 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 47 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 48 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 49 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 50 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 51 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 52 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 53 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 54 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 55 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 56 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 57 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 58 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 59 | github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk= 60 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 61 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 62 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 63 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 64 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 65 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 66 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 67 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 68 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 69 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 70 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 71 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 72 | github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= 73 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 74 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 75 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 76 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 77 | github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= 78 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 79 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 80 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 81 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 82 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 83 | github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8= 84 | github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= 85 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 86 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 87 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 88 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 89 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 90 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 91 | github.com/klauspost/compress v1.9.8 h1:VMAMUUOh+gaxKTMk+zqbjsSjsIcUcL/LF4o63i82QyA= 92 | github.com/klauspost/compress v1.9.8/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 93 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 94 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 95 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 96 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 97 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 98 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 99 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 100 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 101 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 102 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 103 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 104 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 105 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 106 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 107 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 108 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 109 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 110 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 111 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 112 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 113 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 114 | github.com/pierrec/lz4 v2.4.1+incompatible h1:mFe7ttWaflA46Mhqh+jUfjp2qTbPYxLB2/OyBppH9dg= 115 | github.com/pierrec/lz4 v2.4.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 116 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 117 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 118 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 119 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 120 | github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8= 121 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 122 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 123 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 124 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= 125 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 126 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 127 | github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= 128 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 129 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 130 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY= 131 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 132 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 133 | github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563 h1:dY6ETXrvDG7Sa4vE8ZQG4yqWg6UnOcbqTAahkV813vQ= 134 | github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 135 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 136 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 137 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 138 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 139 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 140 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 141 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 142 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 143 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 144 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 145 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 146 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 147 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 148 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 149 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 150 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 151 | github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs= 152 | github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 153 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 154 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 155 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 156 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 157 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 158 | github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E= 159 | github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= 160 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 161 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 162 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 163 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 164 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 165 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 166 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 167 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 168 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 169 | github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= 170 | github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= 171 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 172 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 173 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 174 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 175 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 176 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 177 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 178 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 179 | golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72 h1:+ELyKg6m8UBf0nPFSqD0mi7zUfwPyXo23HNjMnXPz7w= 180 | golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 181 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 182 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 183 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 184 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 185 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 186 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 187 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 188 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 189 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 190 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 191 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 192 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 193 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= 194 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 195 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 196 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 197 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 198 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 199 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 200 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 201 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 202 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 203 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 204 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 205 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 206 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 207 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA= 208 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 209 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 210 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 211 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 212 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 213 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 214 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 215 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 216 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 217 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 218 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 219 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 220 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 221 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 222 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 223 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 224 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 225 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= 226 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 227 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 228 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 229 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 230 | google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk= 231 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 232 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 233 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 234 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 235 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 236 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 237 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 238 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 239 | gopkg.in/jcmturner/aescts.v1 v1.0.1 h1:cVVZBK2b1zY26haWB4vbBiZrfFQnfbTVrE3xZq6hrEw= 240 | gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo= 241 | gopkg.in/jcmturner/dnsutils.v1 v1.0.1 h1:cIuC1OLRGZrld+16ZJvvZxVJeKPsvd5eUIvxfoN5hSM= 242 | gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q= 243 | gopkg.in/jcmturner/goidentity.v3 v3.0.0 h1:1duIyWiTaYvVx3YX2CYtpJbUFd7/UuPYCfgXtQ3VTbI= 244 | gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4= 245 | gopkg.in/jcmturner/gokrb5.v7 v7.5.0 h1:a9tsXlIDD9SKxotJMK3niV7rPZAJeX2aD/0yg3qlIrg= 246 | gopkg.in/jcmturner/gokrb5.v7 v7.5.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM= 247 | gopkg.in/jcmturner/rpc.v1 v1.1.0 h1:QHIUxTX1ISuAv9dD2wJ9HWQVuWDX/Zc0PfeC2tjc4rU= 248 | gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8= 249 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 250 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 251 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 252 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 253 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 254 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 255 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 256 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 257 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs= 258 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 259 | -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-msk-content-streaming/a78fdfdcd625d6640e66c3c998aa4dcf7a8efe88/images/architecture.png -------------------------------------------------------------------------------- /images/c9after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-msk-content-streaming/a78fdfdcd625d6640e66c3c998aa4dcf7a8efe88/images/c9after.png -------------------------------------------------------------------------------- /images/c9before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-msk-content-streaming/a78fdfdcd625d6640e66c3c998aa4dcf7a8efe88/images/c9before.png -------------------------------------------------------------------------------- /images/c9preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-msk-content-streaming/a78fdfdcd625d6640e66c3c998aa4dcf7a8efe88/images/c9preview.png -------------------------------------------------------------------------------- /images/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-msk-content-streaming/a78fdfdcd625d6640e66c3c998aa4dcf7a8efe88/images/example.gif -------------------------------------------------------------------------------- /mesh/mesh.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec": {} 3 | } 4 | -------------------------------------------------------------------------------- /mesh/serverNode.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec": { 3 | "backends": [], 4 | "listeners": [ 5 | { 6 | "healthCheck": { 7 | "healthyThreshold": 2, 8 | "intervalMillis": 5000, 9 | "port": 9090, 10 | "protocol": "grpc", 11 | "timeoutMillis": 2000, 12 | "unhealthyThreshold": 3 13 | }, 14 | "portMapping": { 15 | "port": 9090, 16 | "protocol": "grpc" 17 | } 18 | } 19 | ], 20 | "serviceDiscovery": { 21 | "awsCloudMap": { 22 | "namespaceName": "content.local", 23 | "serviceName": "server" 24 | } 25 | } 26 | }, 27 | "virtualNodeName": "server" 28 | } 29 | -------------------------------------------------------------------------------- /mesh/serverRoute.json: -------------------------------------------------------------------------------- 1 | { 2 | "virtualRouterName": "virtual-router", 3 | "routeName": "server", 4 | "spec": { 5 | "grpcRoute": { 6 | "action": { 7 | "weightedTargets": [ 8 | { 9 | "virtualNode": "server", 10 | "weight": 100 11 | } 12 | ] 13 | }, 14 | "match": { 15 | "serviceName": "proto.Micro" 16 | }, 17 | "timeout": { 18 | "idle": { 19 | "unit": "s", 20 | "value": 0 21 | } 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /mesh/serverRouter.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec": { 3 | "listeners": [ 4 | { 5 | "portMapping": { 6 | "port": 9090, 7 | "protocol": "grpc" 8 | } 9 | } 10 | ] 11 | }, 12 | "virtualRouterName": "virtual-router" 13 | } 14 | -------------------------------------------------------------------------------- /mesh/serverService.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec": { 3 | "provider": { 4 | "virtualRouter": { 5 | "virtualRouterName": "virtual-router" 6 | } 7 | } 8 | }, 9 | "virtualServiceName": "server.content.local" 10 | } 11 | -------------------------------------------------------------------------------- /proto/proto.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: proto.proto 3 | 4 | package proto 5 | 6 | import ( 7 | context "context" 8 | fmt "fmt" 9 | proto "github.com/golang/protobuf/proto" 10 | _ "github.com/golang/protobuf/ptypes/timestamp" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | math "math" 15 | ) 16 | 17 | // Reference imports to suppress errors if they are not otherwise used. 18 | var _ = proto.Marshal 19 | var _ = fmt.Errorf 20 | var _ = math.Inf 21 | 22 | // This is a compile-time assertion to ensure that this generated file 23 | // is compatible with the proto package it is being compiled against. 24 | // A compilation error at this line likely means your copy of the 25 | // proto package needs to be updated. 26 | const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package 27 | 28 | // Item ... 29 | type Item struct { 30 | // Types that are valid to be assigned to Item: 31 | // *Item_Article 32 | Item isItem_Item `protobuf_oneof:"item"` 33 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 34 | XXX_unrecognized []byte `json:"-"` 35 | XXX_sizecache int32 `json:"-"` 36 | } 37 | 38 | func (m *Item) Reset() { *m = Item{} } 39 | func (m *Item) String() string { return proto.CompactTextString(m) } 40 | func (*Item) ProtoMessage() {} 41 | func (*Item) Descriptor() ([]byte, []int) { 42 | return fileDescriptor_2fcc84b9998d60d8, []int{0} 43 | } 44 | 45 | func (m *Item) XXX_Unmarshal(b []byte) error { 46 | return xxx_messageInfo_Item.Unmarshal(m, b) 47 | } 48 | func (m *Item) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 49 | return xxx_messageInfo_Item.Marshal(b, m, deterministic) 50 | } 51 | func (m *Item) XXX_Merge(src proto.Message) { 52 | xxx_messageInfo_Item.Merge(m, src) 53 | } 54 | func (m *Item) XXX_Size() int { 55 | return xxx_messageInfo_Item.Size(m) 56 | } 57 | func (m *Item) XXX_DiscardUnknown() { 58 | xxx_messageInfo_Item.DiscardUnknown(m) 59 | } 60 | 61 | var xxx_messageInfo_Item proto.InternalMessageInfo 62 | 63 | type isItem_Item interface { 64 | isItem_Item() 65 | } 66 | 67 | type Item_Article struct { 68 | Article *Article `protobuf:"bytes,10,opt,name=article,proto3,oneof"` 69 | } 70 | 71 | func (*Item_Article) isItem_Item() {} 72 | 73 | func (m *Item) GetItem() isItem_Item { 74 | if m != nil { 75 | return m.Item 76 | } 77 | return nil 78 | } 79 | 80 | func (m *Item) GetArticle() *Article { 81 | if x, ok := m.GetItem().(*Item_Article); ok { 82 | return x.Article 83 | } 84 | return nil 85 | } 86 | 87 | // XXX_OneofWrappers is for the internal use of the proto package. 88 | func (*Item) XXX_OneofWrappers() []interface{} { 89 | return []interface{}{ 90 | (*Item_Article)(nil), 91 | } 92 | } 93 | 94 | // Article ... 95 | type Article struct { 96 | // UUID ... 97 | Uuid string `protobuf:"bytes,1,opt,name=uuid,proto3" json:"uuid,omitempty"` 98 | // Title ... 99 | Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` 100 | // Body ... 101 | Body string `protobuf:"bytes,3,opt,name=body,proto3" json:"body,omitempty"` 102 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 103 | XXX_unrecognized []byte `json:"-"` 104 | XXX_sizecache int32 `json:"-"` 105 | } 106 | 107 | func (m *Article) Reset() { *m = Article{} } 108 | func (m *Article) String() string { return proto.CompactTextString(m) } 109 | func (*Article) ProtoMessage() {} 110 | func (*Article) Descriptor() ([]byte, []int) { 111 | return fileDescriptor_2fcc84b9998d60d8, []int{1} 112 | } 113 | 114 | func (m *Article) XXX_Unmarshal(b []byte) error { 115 | return xxx_messageInfo_Article.Unmarshal(m, b) 116 | } 117 | func (m *Article) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 118 | return xxx_messageInfo_Article.Marshal(b, m, deterministic) 119 | } 120 | func (m *Article) XXX_Merge(src proto.Message) { 121 | xxx_messageInfo_Article.Merge(m, src) 122 | } 123 | func (m *Article) XXX_Size() int { 124 | return xxx_messageInfo_Article.Size(m) 125 | } 126 | func (m *Article) XXX_DiscardUnknown() { 127 | xxx_messageInfo_Article.DiscardUnknown(m) 128 | } 129 | 130 | var xxx_messageInfo_Article proto.InternalMessageInfo 131 | 132 | func (m *Article) GetUuid() string { 133 | if m != nil { 134 | return m.Uuid 135 | } 136 | return "" 137 | } 138 | 139 | func (m *Article) GetTitle() string { 140 | if m != nil { 141 | return m.Title 142 | } 143 | return "" 144 | } 145 | 146 | func (m *Article) GetBody() string { 147 | if m != nil { 148 | return m.Body 149 | } 150 | return "" 151 | } 152 | 153 | // Insert ... 154 | type Insert struct { 155 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 156 | XXX_unrecognized []byte `json:"-"` 157 | XXX_sizecache int32 `json:"-"` 158 | } 159 | 160 | func (m *Insert) Reset() { *m = Insert{} } 161 | func (m *Insert) String() string { return proto.CompactTextString(m) } 162 | func (*Insert) ProtoMessage() {} 163 | func (*Insert) Descriptor() ([]byte, []int) { 164 | return fileDescriptor_2fcc84b9998d60d8, []int{2} 165 | } 166 | 167 | func (m *Insert) XXX_Unmarshal(b []byte) error { 168 | return xxx_messageInfo_Insert.Unmarshal(m, b) 169 | } 170 | func (m *Insert) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 171 | return xxx_messageInfo_Insert.Marshal(b, m, deterministic) 172 | } 173 | func (m *Insert) XXX_Merge(src proto.Message) { 174 | xxx_messageInfo_Insert.Merge(m, src) 175 | } 176 | func (m *Insert) XXX_Size() int { 177 | return xxx_messageInfo_Insert.Size(m) 178 | } 179 | func (m *Insert) XXX_DiscardUnknown() { 180 | xxx_messageInfo_Insert.DiscardUnknown(m) 181 | } 182 | 183 | var xxx_messageInfo_Insert proto.InternalMessageInfo 184 | 185 | // Request ... 186 | type Insert_Request struct { 187 | Item *Item `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` 188 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 189 | XXX_unrecognized []byte `json:"-"` 190 | XXX_sizecache int32 `json:"-"` 191 | } 192 | 193 | func (m *Insert_Request) Reset() { *m = Insert_Request{} } 194 | func (m *Insert_Request) String() string { return proto.CompactTextString(m) } 195 | func (*Insert_Request) ProtoMessage() {} 196 | func (*Insert_Request) Descriptor() ([]byte, []int) { 197 | return fileDescriptor_2fcc84b9998d60d8, []int{2, 0} 198 | } 199 | 200 | func (m *Insert_Request) XXX_Unmarshal(b []byte) error { 201 | return xxx_messageInfo_Insert_Request.Unmarshal(m, b) 202 | } 203 | func (m *Insert_Request) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 204 | return xxx_messageInfo_Insert_Request.Marshal(b, m, deterministic) 205 | } 206 | func (m *Insert_Request) XXX_Merge(src proto.Message) { 207 | xxx_messageInfo_Insert_Request.Merge(m, src) 208 | } 209 | func (m *Insert_Request) XXX_Size() int { 210 | return xxx_messageInfo_Insert_Request.Size(m) 211 | } 212 | func (m *Insert_Request) XXX_DiscardUnknown() { 213 | xxx_messageInfo_Insert_Request.DiscardUnknown(m) 214 | } 215 | 216 | var xxx_messageInfo_Insert_Request proto.InternalMessageInfo 217 | 218 | func (m *Insert_Request) GetItem() *Item { 219 | if m != nil { 220 | return m.Item 221 | } 222 | return nil 223 | } 224 | 225 | // Response ... 226 | type Insert_Response struct { 227 | Uuid string `protobuf:"bytes,1,opt,name=uuid,proto3" json:"uuid,omitempty"` 228 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 229 | XXX_unrecognized []byte `json:"-"` 230 | XXX_sizecache int32 `json:"-"` 231 | } 232 | 233 | func (m *Insert_Response) Reset() { *m = Insert_Response{} } 234 | func (m *Insert_Response) String() string { return proto.CompactTextString(m) } 235 | func (*Insert_Response) ProtoMessage() {} 236 | func (*Insert_Response) Descriptor() ([]byte, []int) { 237 | return fileDescriptor_2fcc84b9998d60d8, []int{2, 1} 238 | } 239 | 240 | func (m *Insert_Response) XXX_Unmarshal(b []byte) error { 241 | return xxx_messageInfo_Insert_Response.Unmarshal(m, b) 242 | } 243 | func (m *Insert_Response) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 244 | return xxx_messageInfo_Insert_Response.Marshal(b, m, deterministic) 245 | } 246 | func (m *Insert_Response) XXX_Merge(src proto.Message) { 247 | xxx_messageInfo_Insert_Response.Merge(m, src) 248 | } 249 | func (m *Insert_Response) XXX_Size() int { 250 | return xxx_messageInfo_Insert_Response.Size(m) 251 | } 252 | func (m *Insert_Response) XXX_DiscardUnknown() { 253 | xxx_messageInfo_Insert_Response.DiscardUnknown(m) 254 | } 255 | 256 | var xxx_messageInfo_Insert_Response proto.InternalMessageInfo 257 | 258 | func (m *Insert_Response) GetUuid() string { 259 | if m != nil { 260 | return m.Uuid 261 | } 262 | return "" 263 | } 264 | 265 | // Update ... 266 | type Update struct { 267 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 268 | XXX_unrecognized []byte `json:"-"` 269 | XXX_sizecache int32 `json:"-"` 270 | } 271 | 272 | func (m *Update) Reset() { *m = Update{} } 273 | func (m *Update) String() string { return proto.CompactTextString(m) } 274 | func (*Update) ProtoMessage() {} 275 | func (*Update) Descriptor() ([]byte, []int) { 276 | return fileDescriptor_2fcc84b9998d60d8, []int{3} 277 | } 278 | 279 | func (m *Update) XXX_Unmarshal(b []byte) error { 280 | return xxx_messageInfo_Update.Unmarshal(m, b) 281 | } 282 | func (m *Update) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 283 | return xxx_messageInfo_Update.Marshal(b, m, deterministic) 284 | } 285 | func (m *Update) XXX_Merge(src proto.Message) { 286 | xxx_messageInfo_Update.Merge(m, src) 287 | } 288 | func (m *Update) XXX_Size() int { 289 | return xxx_messageInfo_Update.Size(m) 290 | } 291 | func (m *Update) XXX_DiscardUnknown() { 292 | xxx_messageInfo_Update.DiscardUnknown(m) 293 | } 294 | 295 | var xxx_messageInfo_Update proto.InternalMessageInfo 296 | 297 | // Request ... 298 | type Update_Request struct { 299 | Item *Item `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` 300 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 301 | XXX_unrecognized []byte `json:"-"` 302 | XXX_sizecache int32 `json:"-"` 303 | } 304 | 305 | func (m *Update_Request) Reset() { *m = Update_Request{} } 306 | func (m *Update_Request) String() string { return proto.CompactTextString(m) } 307 | func (*Update_Request) ProtoMessage() {} 308 | func (*Update_Request) Descriptor() ([]byte, []int) { 309 | return fileDescriptor_2fcc84b9998d60d8, []int{3, 0} 310 | } 311 | 312 | func (m *Update_Request) XXX_Unmarshal(b []byte) error { 313 | return xxx_messageInfo_Update_Request.Unmarshal(m, b) 314 | } 315 | func (m *Update_Request) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 316 | return xxx_messageInfo_Update_Request.Marshal(b, m, deterministic) 317 | } 318 | func (m *Update_Request) XXX_Merge(src proto.Message) { 319 | xxx_messageInfo_Update_Request.Merge(m, src) 320 | } 321 | func (m *Update_Request) XXX_Size() int { 322 | return xxx_messageInfo_Update_Request.Size(m) 323 | } 324 | func (m *Update_Request) XXX_DiscardUnknown() { 325 | xxx_messageInfo_Update_Request.DiscardUnknown(m) 326 | } 327 | 328 | var xxx_messageInfo_Update_Request proto.InternalMessageInfo 329 | 330 | func (m *Update_Request) GetItem() *Item { 331 | if m != nil { 332 | return m.Item 333 | } 334 | return nil 335 | } 336 | 337 | // Response ... 338 | type Update_Response struct { 339 | Uuid string `protobuf:"bytes,1,opt,name=uuid,proto3" json:"uuid,omitempty"` 340 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 341 | XXX_unrecognized []byte `json:"-"` 342 | XXX_sizecache int32 `json:"-"` 343 | } 344 | 345 | func (m *Update_Response) Reset() { *m = Update_Response{} } 346 | func (m *Update_Response) String() string { return proto.CompactTextString(m) } 347 | func (*Update_Response) ProtoMessage() {} 348 | func (*Update_Response) Descriptor() ([]byte, []int) { 349 | return fileDescriptor_2fcc84b9998d60d8, []int{3, 1} 350 | } 351 | 352 | func (m *Update_Response) XXX_Unmarshal(b []byte) error { 353 | return xxx_messageInfo_Update_Response.Unmarshal(m, b) 354 | } 355 | func (m *Update_Response) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 356 | return xxx_messageInfo_Update_Response.Marshal(b, m, deterministic) 357 | } 358 | func (m *Update_Response) XXX_Merge(src proto.Message) { 359 | xxx_messageInfo_Update_Response.Merge(m, src) 360 | } 361 | func (m *Update_Response) XXX_Size() int { 362 | return xxx_messageInfo_Update_Response.Size(m) 363 | } 364 | func (m *Update_Response) XXX_DiscardUnknown() { 365 | xxx_messageInfo_Update_Response.DiscardUnknown(m) 366 | } 367 | 368 | var xxx_messageInfo_Update_Response proto.InternalMessageInfo 369 | 370 | func (m *Update_Response) GetUuid() string { 371 | if m != nil { 372 | return m.Uuid 373 | } 374 | return "" 375 | } 376 | 377 | // ListArticles ... 378 | type ListArticles struct { 379 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 380 | XXX_unrecognized []byte `json:"-"` 381 | XXX_sizecache int32 `json:"-"` 382 | } 383 | 384 | func (m *ListArticles) Reset() { *m = ListArticles{} } 385 | func (m *ListArticles) String() string { return proto.CompactTextString(m) } 386 | func (*ListArticles) ProtoMessage() {} 387 | func (*ListArticles) Descriptor() ([]byte, []int) { 388 | return fileDescriptor_2fcc84b9998d60d8, []int{4} 389 | } 390 | 391 | func (m *ListArticles) XXX_Unmarshal(b []byte) error { 392 | return xxx_messageInfo_ListArticles.Unmarshal(m, b) 393 | } 394 | func (m *ListArticles) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 395 | return xxx_messageInfo_ListArticles.Marshal(b, m, deterministic) 396 | } 397 | func (m *ListArticles) XXX_Merge(src proto.Message) { 398 | xxx_messageInfo_ListArticles.Merge(m, src) 399 | } 400 | func (m *ListArticles) XXX_Size() int { 401 | return xxx_messageInfo_ListArticles.Size(m) 402 | } 403 | func (m *ListArticles) XXX_DiscardUnknown() { 404 | xxx_messageInfo_ListArticles.DiscardUnknown(m) 405 | } 406 | 407 | var xxx_messageInfo_ListArticles proto.InternalMessageInfo 408 | 409 | // Request ... 410 | type ListArticles_Request struct { 411 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 412 | XXX_unrecognized []byte `json:"-"` 413 | XXX_sizecache int32 `json:"-"` 414 | } 415 | 416 | func (m *ListArticles_Request) Reset() { *m = ListArticles_Request{} } 417 | func (m *ListArticles_Request) String() string { return proto.CompactTextString(m) } 418 | func (*ListArticles_Request) ProtoMessage() {} 419 | func (*ListArticles_Request) Descriptor() ([]byte, []int) { 420 | return fileDescriptor_2fcc84b9998d60d8, []int{4, 0} 421 | } 422 | 423 | func (m *ListArticles_Request) XXX_Unmarshal(b []byte) error { 424 | return xxx_messageInfo_ListArticles_Request.Unmarshal(m, b) 425 | } 426 | func (m *ListArticles_Request) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 427 | return xxx_messageInfo_ListArticles_Request.Marshal(b, m, deterministic) 428 | } 429 | func (m *ListArticles_Request) XXX_Merge(src proto.Message) { 430 | xxx_messageInfo_ListArticles_Request.Merge(m, src) 431 | } 432 | func (m *ListArticles_Request) XXX_Size() int { 433 | return xxx_messageInfo_ListArticles_Request.Size(m) 434 | } 435 | func (m *ListArticles_Request) XXX_DiscardUnknown() { 436 | xxx_messageInfo_ListArticles_Request.DiscardUnknown(m) 437 | } 438 | 439 | var xxx_messageInfo_ListArticles_Request proto.InternalMessageInfo 440 | 441 | // Response ... 442 | type ListArticles_Response struct { 443 | Articles []*Article `protobuf:"bytes,1,rep,name=articles,proto3" json:"articles,omitempty"` 444 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 445 | XXX_unrecognized []byte `json:"-"` 446 | XXX_sizecache int32 `json:"-"` 447 | } 448 | 449 | func (m *ListArticles_Response) Reset() { *m = ListArticles_Response{} } 450 | func (m *ListArticles_Response) String() string { return proto.CompactTextString(m) } 451 | func (*ListArticles_Response) ProtoMessage() {} 452 | func (*ListArticles_Response) Descriptor() ([]byte, []int) { 453 | return fileDescriptor_2fcc84b9998d60d8, []int{4, 1} 454 | } 455 | 456 | func (m *ListArticles_Response) XXX_Unmarshal(b []byte) error { 457 | return xxx_messageInfo_ListArticles_Response.Unmarshal(m, b) 458 | } 459 | func (m *ListArticles_Response) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 460 | return xxx_messageInfo_ListArticles_Response.Marshal(b, m, deterministic) 461 | } 462 | func (m *ListArticles_Response) XXX_Merge(src proto.Message) { 463 | xxx_messageInfo_ListArticles_Response.Merge(m, src) 464 | } 465 | func (m *ListArticles_Response) XXX_Size() int { 466 | return xxx_messageInfo_ListArticles_Response.Size(m) 467 | } 468 | func (m *ListArticles_Response) XXX_DiscardUnknown() { 469 | xxx_messageInfo_ListArticles_Response.DiscardUnknown(m) 470 | } 471 | 472 | var xxx_messageInfo_ListArticles_Response proto.InternalMessageInfo 473 | 474 | func (m *ListArticles_Response) GetArticles() []*Article { 475 | if m != nil { 476 | return m.Articles 477 | } 478 | return nil 479 | } 480 | 481 | // Empty ... 482 | type Empty struct { 483 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 484 | XXX_unrecognized []byte `json:"-"` 485 | XXX_sizecache int32 `json:"-"` 486 | } 487 | 488 | func (m *Empty) Reset() { *m = Empty{} } 489 | func (m *Empty) String() string { return proto.CompactTextString(m) } 490 | func (*Empty) ProtoMessage() {} 491 | func (*Empty) Descriptor() ([]byte, []int) { 492 | return fileDescriptor_2fcc84b9998d60d8, []int{5} 493 | } 494 | 495 | func (m *Empty) XXX_Unmarshal(b []byte) error { 496 | return xxx_messageInfo_Empty.Unmarshal(m, b) 497 | } 498 | func (m *Empty) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 499 | return xxx_messageInfo_Empty.Marshal(b, m, deterministic) 500 | } 501 | func (m *Empty) XXX_Merge(src proto.Message) { 502 | xxx_messageInfo_Empty.Merge(m, src) 503 | } 504 | func (m *Empty) XXX_Size() int { 505 | return xxx_messageInfo_Empty.Size(m) 506 | } 507 | func (m *Empty) XXX_DiscardUnknown() { 508 | xxx_messageInfo_Empty.DiscardUnknown(m) 509 | } 510 | 511 | var xxx_messageInfo_Empty proto.InternalMessageInfo 512 | 513 | func init() { 514 | proto.RegisterType((*Item)(nil), "proto.Item") 515 | proto.RegisterType((*Article)(nil), "proto.Article") 516 | proto.RegisterType((*Insert)(nil), "proto.Insert") 517 | proto.RegisterType((*Insert_Request)(nil), "proto.Insert.Request") 518 | proto.RegisterType((*Insert_Response)(nil), "proto.Insert.Response") 519 | proto.RegisterType((*Update)(nil), "proto.Update") 520 | proto.RegisterType((*Update_Request)(nil), "proto.Update.Request") 521 | proto.RegisterType((*Update_Response)(nil), "proto.Update.Response") 522 | proto.RegisterType((*ListArticles)(nil), "proto.ListArticles") 523 | proto.RegisterType((*ListArticles_Request)(nil), "proto.ListArticles.Request") 524 | proto.RegisterType((*ListArticles_Response)(nil), "proto.ListArticles.Response") 525 | proto.RegisterType((*Empty)(nil), "proto.Empty") 526 | } 527 | 528 | func init() { 529 | proto.RegisterFile("proto.proto", fileDescriptor_2fcc84b9998d60d8) 530 | } 531 | 532 | var fileDescriptor_2fcc84b9998d60d8 = []byte{ 533 | // 318 bytes of a gzipped FileDescriptorProto 534 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x50, 0xc1, 0x4a, 0xc3, 0x40, 535 | 0x10, 0x75, 0x6d, 0xd3, 0xb4, 0x13, 0xf1, 0xb0, 0xa8, 0x84, 0x28, 0xb6, 0xe4, 0x54, 0x7a, 0x68, 536 | 0x25, 0x82, 0x05, 0x6f, 0x0a, 0xa2, 0x45, 0x3d, 0xb8, 0xe8, 0x07, 0x24, 0xcd, 0x5a, 0x16, 0x92, 537 | 0x6e, 0xcc, 0x4e, 0x0e, 0xfd, 0x41, 0xbf, 0x4b, 0xb2, 0xbb, 0x09, 0xd5, 0xf6, 0xe8, 0x25, 0xd9, 538 | 0x79, 0xef, 0xcd, 0x7b, 0x33, 0x03, 0x5e, 0x51, 0x4a, 0x94, 0x53, 0xfd, 0xa5, 0x8e, 0xfe, 0x05, 539 | 0xc3, 0x95, 0x94, 0xab, 0x8c, 0xcf, 0x74, 0x95, 0x54, 0x9f, 0x33, 0x14, 0x39, 0x57, 0x18, 0xe7, 540 | 0x85, 0xd1, 0x85, 0xb7, 0xd0, 0x5d, 0x20, 0xcf, 0xe9, 0x04, 0xdc, 0xb8, 0x44, 0xb1, 0xcc, 0xb8, 541 | 0x0f, 0x23, 0x32, 0xf6, 0xa2, 0x63, 0x23, 0x98, 0xde, 0x19, 0xf4, 0xe9, 0x80, 0x35, 0x82, 0xfb, 542 | 0x1e, 0x74, 0x05, 0xf2, 0x3c, 0x7c, 0x04, 0xd7, 0xb2, 0x94, 0x42, 0xb7, 0xaa, 0x44, 0xea, 0x93, 543 | 0x11, 0x19, 0x0f, 0x98, 0x7e, 0xd3, 0x13, 0x70, 0x50, 0x60, 0xc6, 0xfd, 0x43, 0x0d, 0x9a, 0xa2, 544 | 0x56, 0x26, 0x32, 0xdd, 0xf8, 0x1d, 0xa3, 0xac, 0xdf, 0xe1, 0x3b, 0xf4, 0x16, 0x6b, 0xc5, 0x4b, 545 | 0x0c, 0x26, 0xe0, 0x32, 0xfe, 0x55, 0x71, 0x85, 0x74, 0x68, 0x52, 0xb4, 0xa5, 0x17, 0x79, 0x76, 546 | 0x9c, 0x7a, 0x58, 0xa6, 0x89, 0xe0, 0x12, 0xfa, 0x8c, 0xab, 0x42, 0xae, 0xd5, 0xde, 0xfc, 0xda, 547 | 0xf5, 0xa3, 0x48, 0x63, 0xe4, 0xff, 0xea, 0xfa, 0x06, 0x47, 0x2f, 0x42, 0xa1, 0x5d, 0x5c, 0x05, 548 | 0x83, 0xd6, 0x3b, 0xb8, 0xd9, 0x6a, 0x9d, 0x40, 0xdf, 0x9e, 0x4b, 0xf9, 0x64, 0xd4, 0xd9, 0x3d, 549 | 0x28, 0x6b, 0xf9, 0xd0, 0x05, 0xe7, 0x21, 0x2f, 0x70, 0x13, 0x7d, 0x13, 0x70, 0x5e, 0xc5, 0xb2, 550 | 0x94, 0x74, 0xde, 0x5c, 0x84, 0x9e, 0x36, 0x23, 0xea, 0x72, 0xda, 0x64, 0x9d, 0xfd, 0x85, 0x6d, 551 | 0xee, 0xbc, 0x59, 0xba, 0x6d, 0x34, 0xe5, 0x4e, 0x63, 0x0b, 0xdb, 0xc6, 0xe7, 0xdf, 0x7b, 0xd1, 552 | 0x73, 0xab, 0xdb, 0x06, 0x5b, 0x93, 0x8b, 0xfd, 0xa4, 0xb1, 0xba, 0x22, 0x49, 0x4f, 0xd3, 0xd7, 553 | 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x1f, 0x6a, 0x70, 0xf3, 0x93, 0x02, 0x00, 0x00, 554 | } 555 | 556 | // Reference imports to suppress errors if they are not otherwise used. 557 | var _ context.Context 558 | var _ grpc.ClientConnInterface 559 | 560 | // This is a compile-time assertion to ensure that this generated file 561 | // is compatible with the grpc package it is being compiled against. 562 | const _ = grpc.SupportPackageIsVersion6 563 | 564 | // MicroClient is the client API for Micro service. 565 | // 566 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. 567 | type MicroClient interface { 568 | // Insert ... 569 | Insert(ctx context.Context, in *Insert_Request, opts ...grpc.CallOption) (*Insert_Response, error) 570 | // Update ... 571 | Update(ctx context.Context, in *Update_Request, opts ...grpc.CallOption) (*Update_Response, error) 572 | // ListArticles ... 573 | ListArticles(ctx context.Context, in *ListArticles_Request, opts ...grpc.CallOption) (Micro_ListArticlesClient, error) 574 | } 575 | 576 | type microClient struct { 577 | cc grpc.ClientConnInterface 578 | } 579 | 580 | func NewMicroClient(cc grpc.ClientConnInterface) MicroClient { 581 | return µClient{cc} 582 | } 583 | 584 | func (c *microClient) Insert(ctx context.Context, in *Insert_Request, opts ...grpc.CallOption) (*Insert_Response, error) { 585 | out := new(Insert_Response) 586 | err := c.cc.Invoke(ctx, "/proto.Micro/Insert", in, out, opts...) 587 | if err != nil { 588 | return nil, err 589 | } 590 | return out, nil 591 | } 592 | 593 | func (c *microClient) Update(ctx context.Context, in *Update_Request, opts ...grpc.CallOption) (*Update_Response, error) { 594 | out := new(Update_Response) 595 | err := c.cc.Invoke(ctx, "/proto.Micro/Update", in, out, opts...) 596 | if err != nil { 597 | return nil, err 598 | } 599 | return out, nil 600 | } 601 | 602 | func (c *microClient) ListArticles(ctx context.Context, in *ListArticles_Request, opts ...grpc.CallOption) (Micro_ListArticlesClient, error) { 603 | stream, err := c.cc.NewStream(ctx, &_Micro_serviceDesc.Streams[0], "/proto.Micro/ListArticles", opts...) 604 | if err != nil { 605 | return nil, err 606 | } 607 | x := µListArticlesClient{stream} 608 | if err := x.ClientStream.SendMsg(in); err != nil { 609 | return nil, err 610 | } 611 | if err := x.ClientStream.CloseSend(); err != nil { 612 | return nil, err 613 | } 614 | return x, nil 615 | } 616 | 617 | type Micro_ListArticlesClient interface { 618 | Recv() (*ListArticles_Response, error) 619 | grpc.ClientStream 620 | } 621 | 622 | type microListArticlesClient struct { 623 | grpc.ClientStream 624 | } 625 | 626 | func (x *microListArticlesClient) Recv() (*ListArticles_Response, error) { 627 | m := new(ListArticles_Response) 628 | if err := x.ClientStream.RecvMsg(m); err != nil { 629 | return nil, err 630 | } 631 | return m, nil 632 | } 633 | 634 | // MicroServer is the server API for Micro service. 635 | type MicroServer interface { 636 | // Insert ... 637 | Insert(context.Context, *Insert_Request) (*Insert_Response, error) 638 | // Update ... 639 | Update(context.Context, *Update_Request) (*Update_Response, error) 640 | // ListArticles ... 641 | ListArticles(*ListArticles_Request, Micro_ListArticlesServer) error 642 | } 643 | 644 | // UnimplementedMicroServer can be embedded to have forward compatible implementations. 645 | type UnimplementedMicroServer struct { 646 | } 647 | 648 | func (*UnimplementedMicroServer) Insert(ctx context.Context, req *Insert_Request) (*Insert_Response, error) { 649 | return nil, status.Errorf(codes.Unimplemented, "method Insert not implemented") 650 | } 651 | func (*UnimplementedMicroServer) Update(ctx context.Context, req *Update_Request) (*Update_Response, error) { 652 | return nil, status.Errorf(codes.Unimplemented, "method Update not implemented") 653 | } 654 | func (*UnimplementedMicroServer) ListArticles(req *ListArticles_Request, srv Micro_ListArticlesServer) error { 655 | return status.Errorf(codes.Unimplemented, "method ListArticles not implemented") 656 | } 657 | 658 | func RegisterMicroServer(s *grpc.Server, srv MicroServer) { 659 | s.RegisterService(&_Micro_serviceDesc, srv) 660 | } 661 | 662 | func _Micro_Insert_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 663 | in := new(Insert_Request) 664 | if err := dec(in); err != nil { 665 | return nil, err 666 | } 667 | if interceptor == nil { 668 | return srv.(MicroServer).Insert(ctx, in) 669 | } 670 | info := &grpc.UnaryServerInfo{ 671 | Server: srv, 672 | FullMethod: "/proto.Micro/Insert", 673 | } 674 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 675 | return srv.(MicroServer).Insert(ctx, req.(*Insert_Request)) 676 | } 677 | return interceptor(ctx, in, info, handler) 678 | } 679 | 680 | func _Micro_Update_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 681 | in := new(Update_Request) 682 | if err := dec(in); err != nil { 683 | return nil, err 684 | } 685 | if interceptor == nil { 686 | return srv.(MicroServer).Update(ctx, in) 687 | } 688 | info := &grpc.UnaryServerInfo{ 689 | Server: srv, 690 | FullMethod: "/proto.Micro/Update", 691 | } 692 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 693 | return srv.(MicroServer).Update(ctx, req.(*Update_Request)) 694 | } 695 | return interceptor(ctx, in, info, handler) 696 | } 697 | 698 | func _Micro_ListArticles_Handler(srv interface{}, stream grpc.ServerStream) error { 699 | m := new(ListArticles_Request) 700 | if err := stream.RecvMsg(m); err != nil { 701 | return err 702 | } 703 | return srv.(MicroServer).ListArticles(m, µListArticlesServer{stream}) 704 | } 705 | 706 | type Micro_ListArticlesServer interface { 707 | Send(*ListArticles_Response) error 708 | grpc.ServerStream 709 | } 710 | 711 | type microListArticlesServer struct { 712 | grpc.ServerStream 713 | } 714 | 715 | func (x *microListArticlesServer) Send(m *ListArticles_Response) error { 716 | return x.ServerStream.SendMsg(m) 717 | } 718 | 719 | var _Micro_serviceDesc = grpc.ServiceDesc{ 720 | ServiceName: "proto.Micro", 721 | HandlerType: (*MicroServer)(nil), 722 | Methods: []grpc.MethodDesc{ 723 | { 724 | MethodName: "Insert", 725 | Handler: _Micro_Insert_Handler, 726 | }, 727 | { 728 | MethodName: "Update", 729 | Handler: _Micro_Update_Handler, 730 | }, 731 | }, 732 | Streams: []grpc.StreamDesc{ 733 | { 734 | StreamName: "ListArticles", 735 | Handler: _Micro_ListArticles_Handler, 736 | ServerStreams: true, 737 | }, 738 | }, 739 | Metadata: "proto.proto", 740 | } 741 | -------------------------------------------------------------------------------- /proto/proto.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package proto; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | 7 | // Micro ... 8 | service Micro { 9 | // Insert ... 10 | rpc Insert(Insert.Request) returns (Insert.Response); 11 | // Update ... 12 | rpc Update(Update.Request) returns (Update.Response); 13 | // ListArticles ... 14 | rpc ListArticles(ListArticles.Request) returns (stream ListArticles.Response); 15 | } 16 | 17 | // Item ... 18 | message Item { 19 | oneof item { 20 | Article article = 10; 21 | } 22 | } 23 | 24 | // Article ... 25 | message Article { 26 | // UUID ... 27 | string uuid = 1; 28 | // Title ... 29 | string title = 2; 30 | // Body ... 31 | string body = 3; 32 | } 33 | 34 | // Insert ... 35 | message Insert { 36 | // Request ... 37 | message Request { 38 | Item item = 1; 39 | } 40 | // Response ... 41 | message Response { 42 | string uuid = 1; 43 | } 44 | } 45 | 46 | // Update ... 47 | message Update { 48 | // Request ... 49 | message Request { 50 | Item item = 1; 51 | } 52 | // Response ... 53 | message Response { 54 | string uuid = 1; 55 | } 56 | } 57 | 58 | // ListArticles ... 59 | message ListArticles { 60 | // Request ... 61 | message Request { 62 | } 63 | // Response ... 64 | message Response { 65 | repeated Article articles = 1; 66 | } 67 | } 68 | 69 | // Empty ... 70 | message Empty {} 71 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # extract the information from the CLOUD9 environment 6 | TMP_ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)" 7 | TMP_DEFAULT_REGION="$(curl -s -m 3 http://169.254.169.254/latest/dynamic/instance-identity/document | jq -c -r .region)" 8 | 9 | # set defaults 10 | PROJECT_NAME="${C9_PROJECT:-$PROJECT_NAME}" 11 | AWS_ACCOUNT_ID="${AWS_ACCOUNT_ID:-$TMP_ACCOUNT_ID}" 12 | AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-$TMP_DEFAULT_REGION}" 13 | 14 | if [ -z $PROJECT_NAME ]; then 15 | echo "PROJECT_NAME environment variable is not set." 16 | exit 1 17 | fi 18 | 19 | if [ -z $AWS_ACCOUNT_ID ]; then 20 | echo "AWS_ACCOUNT_ID environment variable is not set." 21 | exit 1 22 | fi 23 | 24 | if [ -z $AWS_DEFAULT_REGION ]; then 25 | echo "AWS_DEFAULT_REGION environment variable is not set." 26 | exit 1 27 | fi 28 | 29 | if [ -z $KEY_PAIR ]; then 30 | echo "KEYPAIR environment variable is not set." 31 | exit 1 32 | fi 33 | 34 | DIR="$(pwd)" 35 | ECR_IMAGE_PREFIX=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${PROJECT_NAME} 36 | TEMPLATES="$DIR/templates" 37 | 38 | deploy_images() { 39 | echo "Deploying Server images to ECR..." 40 | for app in envoy server; do 41 | aws ecr describe-repositories --repository-name ${PROJECT_NAME}/${app} >/dev/null 2>&1 || aws ecr create-repository --repository-name ${PROJECT_NAME}/${app} 42 | docker build -t ${ECR_IMAGE_PREFIX}/${app} -f ${app}.Dockerfile ${DIR} --build-arg GO_PROXY=${GO_PROXY:-"https://proxy.golang.org"} 43 | $(aws ecr get-login --no-include-email) 44 | docker push ${ECR_IMAGE_PREFIX}/${app} 45 | done 46 | } 47 | 48 | deploy_infra() { 49 | echo "Deploying Cloud Formation stack: \"${PROJECT_NAME}-infra\" containing VPC and Cloud Map namespace..." 50 | aws cloudformation deploy \ 51 | --no-fail-on-empty-changeset \ 52 | --stack-name "${PROJECT_NAME}-infra"\ 53 | --template-file "${TEMPLATES}/infra.yaml" \ 54 | --capabilities CAPABILITY_IAM \ 55 | --parameter-overrides "ProjectName=${PROJECT_NAME}" "KeyPair=${KEY_PAIR}" "ClusterName=${PROJECT_NAME}" 56 | } 57 | 58 | deploy_app() { 59 | echo "Deploying Cloud Formation stack: \"${PROJECT_NAME}-app\" containing ALB, ECS Tasks, and Cloud Map Services..." 60 | aws cloudformation deploy \ 61 | --no-fail-on-empty-changeset \ 62 | --stack-name "${PROJECT_NAME}-app" \ 63 | --template-file "${TEMPLATES}/app.yaml" \ 64 | --capabilities CAPABILITY_IAM \ 65 | --parameter-overrides "ProjectName=${PROJECT_NAME}" "ServerImage=${ECR_IMAGE_PREFIX}/server" "EnvoyFrontImage=${ECR_IMAGE_PREFIX}/envoy" "BootstrapBrokers=$(get_bootstrap_brokers)" 66 | } 67 | 68 | # Save this for later when App Mesh is ready 69 | deploy_mesh() { 70 | mesh_name="${PROJECT_NAME}" 71 | 72 | echo "Deploying Mesh: \"${mesh_name}\"..." 73 | 74 | aws configure add-model \ 75 | --service-name appmesh-preview \ 76 | --service-model https://raw.githubusercontent.com/aws/aws-app-mesh-roadmap/master/appmesh-preview/service-model.json 77 | 78 | aws appmesh-preview create-mesh --mesh-name $mesh_name --cli-input-json file://${DIR}/mesh/mesh.json 79 | aws appmesh-preview create-virtual-node --mesh-name $mesh_name --cli-input-json file://${DIR}/mesh/serverNode.json 80 | aws appmesh-preview create-virtual-router --mesh-name $mesh_name --cli-input-json file://${DIR}/mesh/serverRouter.json 81 | aws appmesh-preview create-virtual-service --mesh-name $mesh_name --cli-input-json file://${DIR}/mesh/serverService.json 82 | aws appmesh-preview create-route --mesh-name $mesh_name --cli-input-json file://${DIR}/mesh/serverRoute.json 83 | 84 | # aws cloudformation deploy \ 85 | # --no-fail-on-empty-changeset \ 86 | # --stack-name "${PROJECT_NAME}-mesh" \ 87 | # --template-file "${DIR}/mesh.yaml" \ 88 | # --capabilities CAPABILITY_IAM \ 89 | # --parameter-overrides "ProjectName=${PROJECT_NAME}" 90 | } 91 | 92 | # Save this for later when App Mesh is ready 93 | delete_mesh() { 94 | mesh_name="${PROJECT_NAME}" 95 | 96 | echo "Deleting Mesh: \"${mesh_name}\"..." 97 | 98 | aws appmesh-preview delete-route --mesh-name $mesh_name --virtual-router-name virtual-router --route-name server 99 | aws appmesh-preview delete-virtual-service --mesh-name $mesh_name --virtual-service-name server.content.local 100 | aws appmesh-preview delete-virtual-router --mesh-name $mesh_name --virtual-router-name virtual-router 101 | aws appmesh-preview delete-virtual-node --mesh-name $mesh_name --virtual-node-name server 102 | aws appmesh-preview delete-mesh --mesh-name $mesh_name 103 | } 104 | 105 | print_bastion() { 106 | ip=$(aws cloudformation describe-stacks \ 107 | --stack-name="${PROJECT_NAME}-infra" \ 108 | --query="Stacks[0].Outputs[?OutputKey=='BastionIp'].OutputValue" \ 109 | --output=text) 110 | echo "${ip}" 111 | } 112 | 113 | print_endpoint() { 114 | prefix=$(aws cloudformation describe-stacks \ 115 | --stack-name="${PROJECT_NAME}-app" \ 116 | --query="Stacks[0].Outputs[?OutputKey=='PublicEndpoint'].OutputValue" \ 117 | --output=text) 118 | echo "${prefix}" 119 | } 120 | 121 | print_bootstrap_brokers() { 122 | echo "Bootstrap Brokers:" 123 | echo "$(get_bootstrap_brokers)" 124 | } 125 | 126 | get_bootstrap_brokers() { 127 | local arn="$@" 128 | local output 129 | 130 | if [ "$arn" == "" ]; then 131 | arn=$(aws cloudformation describe-stacks \ 132 | --stack-name="${PROJECT_NAME}-infra" \ 133 | --query="Stacks[0].Outputs[?OutputKey=='MSKClusterArn'].OutputValue" \ 134 | --output=text) 135 | fi 136 | 137 | output=$(aws kafka get-bootstrap-brokers \ 138 | --cluster-arn="${arn}" \ 139 | --output=text) 140 | 141 | echo $output 142 | } 143 | 144 | deploy_stacks() { 145 | deploy_images 146 | deploy_infra 147 | deploy_app 148 | } 149 | 150 | delete_cfn_stack() { 151 | stack_name=$1 152 | echo "Deleting Cloud Formation stack: \"${stack_name}\"..." 153 | aws cloudformation delete-stack --stack-name $stack_name 154 | echo 'Waiting for the stack to be deleted, this may take a few minutes...' 155 | aws cloudformation wait stack-delete-complete --stack-name $stack_name 156 | echo 'Done' 157 | } 158 | 159 | delete_images() { 160 | for app in server envoy; do 161 | echo "deleting repository \"${app}\"..." 162 | aws ecr delete-repository \ 163 | --repository-name $PROJECT_NAME/$app \ 164 | --force 165 | done 166 | } 167 | 168 | delete_stacks() { 169 | delete_cfn_stack "${PROJECT_NAME}-app" 170 | delete_cfn_stack "${PROJECT_NAME}-infra" 171 | # delete_cfn_stack "${PROJECT_NAME}-mesh" 172 | delete_images 173 | } 174 | 175 | action=${1:-"deploy"} 176 | if [ "$action" == "delete" ]; then 177 | delete_stacks 178 | exit 0 179 | fi 180 | 181 | if [ "$action" == "print_endpoint" ]; then 182 | print_endpoint 183 | exit 0 184 | fi 185 | 186 | deploy_stacks 187 | -------------------------------------------------------------------------------- /scripts/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ./service $@ 3 | -------------------------------------------------------------------------------- /scripts/resize.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Specify the desired volume size in GiB as a command-line argument. If not specified, default to 20 GiB. 4 | SIZE=${1:-20} 5 | 6 | # Install the jq command-line JSON processor. 7 | sudo yum -y install jq 8 | 9 | # Get the ID of the envrionment host Amazon EC2 instance. 10 | INSTANCEID=$(curl http://169.254.169.254/latest/meta-data//instance-id) 11 | 12 | # Get the ID of the Amazon EBS volume associated with the instance. 13 | VOLUMEID=$(aws ec2 describe-instances --instance-id $INSTANCEID | jq -r .Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId) 14 | 15 | # Resize the EBS volume. 16 | aws ec2 modify-volume --volume-id $VOLUMEID --size $SIZE 17 | 18 | # Wait for the resize to finish. 19 | while [ "$(aws ec2 describe-volumes-modifications --volume-id $VOLUMEID --filters Name=modification-state,Values="optimizing","completed" | jq '.VolumesModifications | length')" != "1" ]; do 20 | sleep 5 21 | done 22 | 23 | # Rewrite the partition table so that the partition takes up all the space that it can. 24 | sudo growpart /dev/nvme0n1 1 25 | 26 | # Expand the size of the file system. 27 | sudo resize2fs /dev/nvme0n1p1 28 | -------------------------------------------------------------------------------- /server.Dockerfile: -------------------------------------------------------------------------------- 1 | # build stage 2 | FROM golang:alpine AS build-env 3 | ENV GOPROXY=direct 4 | RUN apk --no-cache add build-base git bzr mercurial gcc 5 | ADD . /src 6 | RUN cd /src && go build -o service server/main.go 7 | 8 | # final stage 9 | FROM alpine 10 | WORKDIR /app 11 | COPY --from=build-env /src/service /app/ 12 | COPY scripts/entrypoint.sh /app/ 13 | RUN chmod +x ./entrypoint.sh 14 | ENTRYPOINT ["./entrypoint.sh"] 15 | CMD [] 16 | -------------------------------------------------------------------------------- /server/cmd/flags.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | func addFlags(cmd *cobra.Command) { 9 | cmd.Flags().String("log-format", "text", "log format") 10 | cmd.Flags().String("log-level", "info", "log-level") 11 | cmd.Flags().String("addr", ":9090", "address") 12 | cmd.Flags().String("topic", "monolog", "kafka, topic") 13 | cmd.Flags().StringSlice("brokers", []string{"localhost:9092"}, "kafka brokers") 14 | cmd.Flags().String("version", "2.3.2", "kafka version") 15 | 16 | // set the link between flags 17 | viper.BindPFlag("log-format", cmd.Flags().Lookup("log-format")) 18 | viper.BindPFlag("log-level", cmd.Flags().Lookup("log-level")) 19 | viper.BindPFlag("addr", cmd.Flags().Lookup("addr")) 20 | viper.BindPFlag("topic", cmd.Flags().Lookup("topic")) 21 | viper.BindPFlag("brokers", cmd.Flags().Lookup("brokers")) 22 | viper.BindPFlag("version", cmd.Flags().Lookup("version")) 23 | } 24 | -------------------------------------------------------------------------------- /server/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | log "github.com/sirupsen/logrus" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | // RootCmd ... 13 | var RootCmd = &cobra.Command{ 14 | Use: "server", 15 | Short: "Runs the gRPC service", 16 | Long: `Not yet`, 17 | RunE: runE, 18 | } 19 | 20 | // Execute adds all child commands to the root command and sets flags appropriately. 21 | // This is called by main.main(). It only needs to happen once to the rootCmd. 22 | func Execute() { 23 | if err := RootCmd.Execute(); err != nil { 24 | fmt.Println(err) 25 | os.Exit(1) 26 | } 27 | } 28 | 29 | func init() { 30 | // initialize cobra 31 | cobra.OnInitialize(initConfig) 32 | 33 | // adding flags 34 | addFlags(RootCmd) 35 | } 36 | 37 | // initConfig reads in config file and ENV variables if set. 38 | func initConfig() { 39 | // set the default format, which is basically text 40 | log.SetFormatter(&log.TextFormatter{}) 41 | 42 | viper.AutomaticEnv() // read in environment variables that match 43 | 44 | // config logger 45 | logConfig() 46 | } 47 | 48 | func logConfig() { 49 | // reset log format 50 | if viper.GetString("log-format") == "json" { 51 | log.SetFormatter(&log.JSONFormatter{}) 52 | } 53 | 54 | // set the configured log level 55 | if level, err := log.ParseLevel(viper.GetString("log-level")); err == nil { 56 | log.SetLevel(level) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /server/cmd/run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/andersnormal/pkg/server" 7 | log "github.com/sirupsen/logrus" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | type root struct { 13 | logger *log.Entry 14 | } 15 | 16 | func runE(cmd *cobra.Command, args []string) error { 17 | // create a new root 18 | root := new(root) 19 | 20 | // init logger 21 | root.logger = log.WithFields(log.Fields{ 22 | "verbose": viper.GetBool("verbose"), 23 | "brokers": viper.GetStringSlice("brokers"), 24 | "topic": viper.GetString("topic"), 25 | }) 26 | 27 | // create root context 28 | ctx, cancel := context.WithCancel(context.Background()) 29 | defer cancel() 30 | 31 | // create server 32 | s, _ := server.WithContext(ctx) 33 | 34 | // log ... 35 | root.logger.Info("Starting server ...") 36 | 37 | // debug listener 38 | debug := server.NewDebugListener( 39 | server.WithPprof(), 40 | server.WithStatusAddr(":8443"), 41 | ) 42 | s.Listen(debug, true) 43 | 44 | // listen for grpc 45 | s.Listen(&srv{}, true) 46 | 47 | // listen for the server and wait for it to fail, 48 | // or for sys interrupts 49 | if err := s.Wait(); err != nil { 50 | root.logger.Error(err) 51 | } 52 | 53 | // noop 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /server/cmd/services.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "math" 7 | "net" 8 | "time" 9 | 10 | pb "github.com/katallaxie/content_streaming_msk/proto" 11 | 12 | "github.com/Shopify/sarama" 13 | "github.com/golang/protobuf/proto" 14 | "github.com/google/uuid" 15 | log "github.com/sirupsen/logrus" 16 | "github.com/spf13/viper" 17 | "google.golang.org/grpc" 18 | "google.golang.org/grpc/codes" 19 | health "google.golang.org/grpc/health/grpc_health_v1" 20 | "google.golang.org/grpc/keepalive" 21 | "google.golang.org/grpc/status" 22 | ) 23 | 24 | type srv struct { 25 | } 26 | 27 | func (s *srv) Start(ctx context.Context, ready func()) func() error { 28 | return func() error { 29 | lis, err := net.Listen("tcp", viper.GetString("addr")) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | sarama.Logger = log.New() 35 | 36 | srv := &service{} 37 | 38 | tlsConfig := &tls.Config{} 39 | tlsConfig.InsecureSkipVerify = true 40 | srv.tlsCfg = tlsConfig 41 | 42 | if err := s.Setup(tlsConfig); err != nil { 43 | return err 44 | } 45 | 46 | var kaep = keepalive.EnforcementPolicy{ 47 | MinTime: 5 * time.Second, // If a client pings more than once every 5 seconds, terminate the connection 48 | PermitWithoutStream: true, // Allow pings even when there are no active streams 49 | } 50 | 51 | var kasp = keepalive.ServerParameters{ 52 | MaxConnectionIdle: time.Duration(math.MaxInt64), // If a client is idle for 15 seconds, send a GOAWAY 53 | MaxConnectionAge: time.Duration(math.MaxInt64), // If any connection is alive for more than 30 seconds, send a GOAWAY 54 | MaxConnectionAgeGrace: 5 * time.Second, // Allow 5 seconds for pending RPCs to complete before forcibly closing connections 55 | Time: 5 * time.Second, // Ping the client if it is idle for 5 seconds to ensure the connection is still active 56 | Timeout: 1 * time.Second, // Wait 1 second for the ping ack before assuming the connection is dead 57 | } 58 | 59 | ss := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp)) 60 | pb.RegisterMicroServer(ss, srv) 61 | health.RegisterHealthServer(ss, srv) 62 | 63 | ready() 64 | 65 | if err := ss.Serve(lis); err != nil { 66 | return err 67 | } 68 | 69 | return nil 70 | } 71 | } 72 | 73 | func (s *srv) Setup(tlsCfg *tls.Config) error { 74 | config := sarama.NewConfig() 75 | config.ClientID = "server" 76 | config.Version = sarama.V2_2_0_0 77 | config.Net.TLS.Enable = true 78 | config.Net.TLS.Config = tlsCfg 79 | 80 | client, err := sarama.NewClient(viper.GetStringSlice("brokers"), config) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | defer client.Close() 86 | 87 | admin, err := sarama.NewClusterAdminFromClient(client) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | defer admin.Close() 93 | 94 | topics, err := admin.ListTopics() 95 | if err != nil { 96 | return err 97 | } 98 | 99 | if _, ok := topics[viper.GetString("topic")]; ok { 100 | return nil 101 | } 102 | 103 | details := &sarama.TopicDetail{ 104 | NumPartitions: 1, 105 | ReplicationFactor: 2, 106 | ConfigEntries: map[string]*string{"cleanup.policy": asString("compact"), "segment.ms": asString("100"), "min.cleanable.dirty.ratio": asString("0.01")}, 107 | } 108 | 109 | return admin.CreateTopic(viper.GetString("topic"), details, false) 110 | } 111 | 112 | type service struct { 113 | tlsCfg *tls.Config 114 | pb.UnimplementedMicroServer 115 | } 116 | 117 | func (s *service) Insert(ctx context.Context, req *pb.Insert_Request) (*pb.Insert_Response, error) { 118 | uuid := uuid.New() 119 | 120 | config := sarama.NewConfig() 121 | config.ClientID = "server" 122 | config.Producer.Return.Successes = true 123 | config.Version = sarama.V2_2_0_0 124 | config.Net.TLS.Enable = true 125 | config.Net.TLS.Config = s.tlsCfg 126 | 127 | producer, err := sarama.NewSyncProducer(viper.GetStringSlice("brokers"), config) 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | defer producer.Close() 133 | 134 | // extracing the item and assigning an uuid. 135 | // the uuid is a message id, so that the log compaction works. 136 | item := req.GetItem() 137 | 138 | switch i := item.Item.(type) { 139 | case *pb.Item_Article: 140 | i.Article.Uuid = uuid.String() 141 | } 142 | 143 | // marshal into new message 144 | b, err := proto.Marshal(item) 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | msg := &sarama.ProducerMessage{Topic: viper.GetString("topic"), Key: sarama.StringEncoder(uuid.String()), Value: sarama.ByteEncoder(b)} 150 | partition, offset, err := producer.SendMessage(msg) 151 | if err != nil { 152 | return nil, err 153 | } 154 | 155 | // logging where we send this 156 | log.Infof("send to partition %s with offset %s", partition, offset) 157 | 158 | return &pb.Insert_Response{Uuid: uuid.String()}, nil 159 | } 160 | 161 | func (s *service) Update(ctx context.Context, req *pb.Update_Request) (*pb.Update_Response, error) { 162 | return nil, status.Errorf(codes.Unimplemented, "method Update not implemented") 163 | } 164 | 165 | func (s *service) ListArticles(req *pb.ListArticles_Request, srv pb.Micro_ListArticlesServer) error { 166 | config := sarama.NewConfig() 167 | config.ClientID = "server" 168 | config.Consumer.Return.Errors = true 169 | config.Version = sarama.V2_2_0_0 170 | config.Net.TLS.Enable = true 171 | config.Net.TLS.Config = s.tlsCfg 172 | 173 | // Create new consumer 174 | master, err := sarama.NewConsumer(viper.GetStringSlice("brokers"), config) 175 | if err != nil { 176 | return err 177 | } 178 | 179 | defer func() { 180 | if err := master.Close(); err != nil { 181 | panic(err) 182 | } 183 | }() 184 | 185 | consumer, errors := consume(viper.GetString("topic"), master) 186 | 187 | for { 188 | select { 189 | case msg := <-consumer: 190 | item := &pb.Item{} 191 | // marshal into new message 192 | if err := proto.Unmarshal(msg.Value, item); err != nil { 193 | return err 194 | } 195 | 196 | if err := srv.Send(&pb.ListArticles_Response{Articles: []*pb.Article{item.GetArticle()}}); err != nil { 197 | return err 198 | } 199 | case err := <-errors: 200 | return err 201 | } 202 | } 203 | } 204 | 205 | func (s *service) Check(ctx context.Context, in *health.HealthCheckRequest) (*health.HealthCheckResponse, error) { 206 | log.Infof("Received Check request: %v", in) 207 | 208 | return &health.HealthCheckResponse{Status: health.HealthCheckResponse_SERVING}, nil 209 | } 210 | 211 | func (s *service) Watch(in *health.HealthCheckRequest, _ health.Health_WatchServer) error { 212 | log.Infof("Received Watch request: %v", in) 213 | 214 | return status.Error(codes.Unimplemented, "unimplemented") 215 | } 216 | 217 | func consume(topic string, master sarama.Consumer) (chan *sarama.ConsumerMessage, chan *sarama.ConsumerError) { 218 | consumers := make(chan *sarama.ConsumerMessage) 219 | errors := make(chan *sarama.ConsumerError) 220 | 221 | partitions, _ := master.Partitions(topic) 222 | // this only consumes partition no 1, you would probably want to consume all partitions 223 | consumer, err := master.ConsumePartition(topic, partitions[0], sarama.OffsetOldest) 224 | if err != nil { 225 | panic(err) 226 | } 227 | 228 | log.Infof(" Start consuming topic ", topic) 229 | 230 | go func(topic string, consumer sarama.PartitionConsumer) { 231 | for { 232 | select { 233 | case consumerError := <-consumer.Errors(): 234 | errors <- consumerError 235 | log.Info("consumerError: %s", consumerError.Err) 236 | case msg := <-consumer.Messages(): 237 | consumers <- msg 238 | log.Infof("Got message on topic ", topic) 239 | } 240 | } 241 | }(topic, consumer) 242 | 243 | return consumers, errors 244 | } 245 | 246 | func asString(s string) *string { 247 | return &s 248 | } 249 | -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/katallaxie/content_streaming_msk/server/cmd" 8 | ) 9 | 10 | func init() { 11 | rand.Seed(time.Now().UnixNano()) 12 | } 13 | 14 | func main() { 15 | cmd.Execute() 16 | } 17 | -------------------------------------------------------------------------------- /templates/app.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "Application Stack for Content Streaming Workshop" 3 | Metadata: 4 | LICENSE: MIT 5 | AWS::CloudFormation::Interface: 6 | ParameterGroups: 7 | - Label: 8 | Detault: Optional Parameters 9 | Parameters: 10 | - EnableAppMesh 11 | - EnableAppMeshPreview 12 | Parameters: 13 | ProjectName: 14 | Type: String 15 | Description: Project name to link stacks 16 | 17 | EnvoyImage: 18 | Type: String 19 | Default: "840364872350.dkr.ecr.us-west-2.amazonaws.com/aws-appmesh-envoy:v1.12.2.1-prod" 20 | Description: Envoy container image 21 | 22 | BootstrapBrokers: 23 | Type: String 24 | Description: List of MSK Brokers 25 | 26 | EnvoyFrontImage: 27 | Type: String 28 | Description: Frontend envoy image 29 | 30 | ServerImage: 31 | Type: String 32 | Description: Server app container image 33 | 34 | ContainerPort: 35 | Type: Number 36 | Description: Port number to use for applications 37 | Default: 9090 38 | 39 | EnableAppMesh: 40 | AllowedValues: 41 | - "true" 42 | - "false" 43 | Default: "false" 44 | Description: "if true it enables the service on a service mesh" 45 | Type: String 46 | 47 | EnableAppMeshPreview: 48 | AllowedValues: 49 | - "true" 50 | - "false" 51 | Default: "false" 52 | Description: "if true it enables the App Mesh preview channel (only available in us-west-2" 53 | Type: String 54 | 55 | LoadBalancerIdleTimeout: 56 | Default: "1800" 57 | Description: "The idle timeout value, in seconds. The valid range is 1-4000 seconds. The default is 60 seconds." 58 | Type: String 59 | 60 | Conditions: 61 | EnableAppMesh: !Equals 62 | - !Ref EnableAppMesh 63 | - "true" 64 | EnableAppMeshPreview: !Equals 65 | - !Ref EnableAppMeshPreview 66 | - "true" 67 | 68 | Resources: 69 | PublicLoadBalancerSecurityGroup: 70 | Type: AWS::EC2::SecurityGroup 71 | Properties: 72 | GroupDescription: "Access to the public facing load balancer" 73 | VpcId: 74 | Fn::ImportValue: !Sub "${ProjectName}:VPC" 75 | SecurityGroupIngress: 76 | - CidrIp: 0.0.0.0/0 77 | IpProtocol: -1 78 | 79 | PublicLoadBalancer: 80 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 81 | Properties: 82 | Scheme: internet-facing 83 | LoadBalancerAttributes: 84 | - Key: idle_timeout.timeout_seconds 85 | Value: !Ref LoadBalancerIdleTimeout 86 | Subnets: 87 | - Fn::ImportValue: !Sub "${ProjectName}:PublicSubnet1" 88 | - Fn::ImportValue: !Sub "${ProjectName}:PublicSubnet2" 89 | SecurityGroups: 90 | - !Ref PublicLoadBalancerSecurityGroup 91 | 92 | WebTargetGroup: 93 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 94 | Properties: 95 | HealthCheckIntervalSeconds: 6 96 | HealthCheckPath: "/server_info" 97 | HealthCheckProtocol: HTTP 98 | HealthCheckTimeoutSeconds: 5 99 | HealthyThresholdCount: 2 100 | HealthCheckPort: 8001 101 | TargetType: ip 102 | Name: !Sub "${ProjectName}-webtarget" 103 | Port: 80 104 | Protocol: HTTP 105 | UnhealthyThresholdCount: 2 106 | TargetGroupAttributes: 107 | - Key: deregistration_delay.timeout_seconds 108 | Value: 120 109 | VpcId: 110 | Fn::ImportValue: !Sub "${ProjectName}:VPC" 111 | 112 | PublicLoadBalancerListener: 113 | Type: AWS::ElasticLoadBalancingV2::Listener 114 | Properties: 115 | DefaultActions: 116 | - TargetGroupArn: !Ref WebTargetGroup 117 | Type: "forward" 118 | LoadBalancerArn: !Ref PublicLoadBalancer 119 | Port: 80 120 | Protocol: HTTP 121 | 122 | WebLoadBalancerRule: 123 | Type: AWS::ElasticLoadBalancingV2::ListenerRule 124 | Properties: 125 | Actions: 126 | - TargetGroupArn: !Ref WebTargetGroup 127 | Type: "forward" 128 | Conditions: 129 | - Field: path-pattern 130 | Values: 131 | - "*" 132 | ListenerArn: !Ref PublicLoadBalancerListener 133 | Priority: 1 134 | 135 | TaskSecurityGroup: 136 | Type: AWS::EC2::SecurityGroup 137 | Properties: 138 | GroupDescription: "Security group for the tasks" 139 | VpcId: 140 | Fn::ImportValue: !Sub "${ProjectName}:VPC" 141 | SecurityGroupIngress: 142 | - CidrIp: 143 | Fn::ImportValue: !Sub "${ProjectName}:VpcCIDR" 144 | IpProtocol: -1 145 | 146 | LogGroup: 147 | Type: AWS::Logs::LogGroup 148 | Properties: 149 | LogGroupName: !Sub "${ProjectName}-log-group" 150 | RetentionInDays: 30 151 | 152 | TaskIamRole: 153 | Type: AWS::IAM::Role 154 | Properties: 155 | Path: / 156 | AssumeRolePolicyDocument: | 157 | { 158 | "Statement": [{ 159 | "Effect": "Allow", 160 | "Principal": { "Service": [ "ecs-tasks.amazonaws.com" ]}, 161 | "Action": [ "sts:AssumeRole" ] 162 | }] 163 | } 164 | ManagedPolicyArns: 165 | - arn:aws:iam::aws:policy/CloudWatchFullAccess 166 | - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess 167 | - arn:aws:iam::aws:policy/AWSAppMeshEnvoyAccess 168 | - !If 169 | - EnableAppMeshPreview 170 | - arn:aws:iam::aws:policy/AWSAppMeshPreviewEnvoyAccess 171 | - !Ref AWS::NoValue 172 | 173 | TaskExecutionIamRole: 174 | Type: AWS::IAM::Role 175 | Properties: 176 | Path: / 177 | AssumeRolePolicyDocument: | 178 | { 179 | "Statement": [{ 180 | "Effect": "Allow", 181 | "Principal": { "Service": [ "ecs-tasks.amazonaws.com" ]}, 182 | "Action": [ "sts:AssumeRole" ] 183 | }] 184 | } 185 | ManagedPolicyArns: 186 | - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly 187 | - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess 188 | 189 | SecurityGroup: 190 | Type: AWS::EC2::SecurityGroup 191 | Properties: 192 | GroupDescription: "Security group for the instances" 193 | VpcId: 194 | Fn::ImportValue: !Sub "${ProjectName}:VPC" 195 | SecurityGroupIngress: 196 | - CidrIp: 197 | Fn::ImportValue: !Sub "${ProjectName}:VpcCIDR" 198 | IpProtocol: -1 199 | 200 | EnvoyFrontTask: 201 | Type: AWS::ECS::TaskDefinition 202 | Properties: 203 | RequiresCompatibilities: 204 | - "FARGATE" 205 | Family: "envoy" 206 | NetworkMode: "awsvpc" 207 | Cpu: 256 208 | Memory: 512 209 | TaskRoleArn: !Ref TaskIamRole 210 | ExecutionRoleArn: !Ref TaskExecutionIamRole 211 | ContainerDefinitions: 212 | - Name: "envoy" 213 | Image: !Ref EnvoyFrontImage 214 | Essential: true 215 | PortMappings: 216 | - ContainerPort: 8080 217 | Protocol: "tcp" 218 | - ContainerPort: 8001 219 | Protocol: "tcp" 220 | Ulimits: 221 | - Name: "nofile" 222 | HardLimit: 15000 223 | SoftLimit: 15000 224 | HealthCheck: 225 | Command: 226 | - "CMD-SHELL" 227 | - "curl -s http://localhost:8001/server_info | grep state | grep -q LIVE" 228 | Interval: 5 229 | Timeout: 2 230 | Retries: 3 231 | LogConfiguration: 232 | LogDriver: "awslogs" 233 | Options: 234 | awslogs-group: !Sub "${ProjectName}-log-group" 235 | awslogs-region: !Ref AWS::Region 236 | awslogs-stream-prefix: "envoy-frontend" 237 | 238 | FrontendService: 239 | Type: AWS::ECS::Service 240 | DependsOn: 241 | - WebLoadBalancerRule 242 | Properties: 243 | Cluster: 244 | Fn::ImportValue: !Sub "${ProjectName}:ECSCluster" 245 | DeploymentConfiguration: 246 | MaximumPercent: 200 247 | MinimumHealthyPercent: 100 248 | DesiredCount: 1 249 | LaunchType: "FARGATE" 250 | NetworkConfiguration: 251 | AwsvpcConfiguration: 252 | AssignPublicIp: DISABLED 253 | SecurityGroups: 254 | - !Ref SecurityGroup 255 | Subnets: 256 | - Fn::ImportValue: !Sub "${ProjectName}:PrivateSubnet1" 257 | - Fn::ImportValue: !Sub "${ProjectName}:PrivateSubnet2" 258 | TaskDefinition: !Ref EnvoyFrontTask 259 | LoadBalancers: 260 | - ContainerName: envoy 261 | ContainerPort: 8080 262 | TargetGroupArn: !Ref WebTargetGroup 263 | 264 | ServerTaskDef: 265 | Type: AWS::ECS::TaskDefinition 266 | Properties: 267 | RequiresCompatibilities: 268 | - "FARGATE" 269 | Family: "server" 270 | NetworkMode: "awsvpc" 271 | Cpu: 256 272 | Memory: 512 273 | TaskRoleArn: !Ref TaskIamRole 274 | ExecutionRoleArn: !Ref TaskExecutionIamRole 275 | ProxyConfiguration: !If 276 | - EnableAppMesh 277 | - Type: "APPMESH" 278 | ContainerName: "envoy" 279 | ProxyConfigurationProperties: 280 | - Name: "IgnoredUID" 281 | Value: "1337" 282 | - Name: "ProxyIngressPort" 283 | Value: "15000" 284 | - Name: "ProxyEgressPort" 285 | Value: "15001" 286 | - Name: "AppPorts" 287 | Value: !Sub "${ContainerPort}" 288 | - Name: "EgressIgnoredPorts" 289 | Value: "2181,9094,9092" 290 | - Name: "EgressIgnoredIPs" 291 | Value: "169.254.170.2,169.254.169.254" 292 | - !Ref AWS::NoValue 293 | ContainerDefinitions: 294 | - Name: "app" 295 | Image: !Ref ServerImage 296 | Essential: true 297 | LogConfiguration: 298 | LogDriver: "awslogs" 299 | Options: 300 | awslogs-group: !Sub "${ProjectName}-log-group" 301 | awslogs-region: !Ref AWS::Region 302 | awslogs-stream-prefix: "server" 303 | PortMappings: 304 | - ContainerPort: !Ref ContainerPort 305 | Protocol: "tcp" 306 | - ContainerPort: 8443 307 | Protocol: "tcp" 308 | Command: 309 | - "--brokers " 310 | - !Ref "BootstrapBrokers" 311 | Environment: 312 | - Name: "PORT" 313 | Value: !Sub "${ContainerPort}" 314 | - !If 315 | - EnableAppMesh 316 | - Name: envoy 317 | Image: !Ref EnvoyImage 318 | Essential: true 319 | User: "1337" 320 | Ulimits: 321 | - Name: "nofile" 322 | HardLimit: 15000 323 | SoftLimit: 15000 324 | PortMappings: 325 | - ContainerPort: 9901 326 | Protocol: "tcp" 327 | - ContainerPort: 15000 328 | Protocol: "tcp" 329 | - ContainerPort: 15001 330 | Protocol: "tcp" 331 | HealthCheck: 332 | Command: 333 | - "CMD-SHELL" 334 | - "curl -s http://localhost:9901/server_info | grep state | grep -q LIVE" 335 | Interval: 5 336 | Timeout: 2 337 | Retries: 3 338 | LogConfiguration: 339 | LogDriver: "awslogs" 340 | Options: 341 | awslogs-group: !Sub "${ProjectName}-log-group" 342 | awslogs-region: !Ref AWS::Region 343 | awslogs-stream-prefix: "server-envoy" 344 | Environment: 345 | - Name: "APPMESH_VIRTUAL_NODE_NAME" 346 | Value: !Sub "mesh/${ProjectName}/virtualNode/server" 347 | - Name: "ENVOY_LOG_LEVEL" 348 | Value: "debug" 349 | - !If 350 | - EnableAppMeshPreview 351 | - Name: "APPMESH_PREVIEW" 352 | Value: 1 353 | - !Ref AWS::NoValue 354 | - !Ref AWS::NoValue 355 | 356 | ServerRegistry: 357 | Type: AWS::ServiceDiscovery::Service 358 | Properties: 359 | Name: "server" 360 | DnsConfig: 361 | NamespaceId: 362 | Fn::ImportValue: !Sub "${ProjectName}:CloudMapNamespaceId" 363 | DnsRecords: 364 | - Type: A 365 | TTL: 300 366 | HealthCheckCustomConfig: 367 | FailureThreshold: 1 368 | 369 | ServerService: 370 | Type: AWS::ECS::Service 371 | DependsOn: 372 | - WebLoadBalancerRule 373 | Properties: 374 | Cluster: 375 | Fn::ImportValue: !Sub "${ProjectName}:ECSCluster" 376 | DeploymentConfiguration: 377 | MaximumPercent: 200 378 | MinimumHealthyPercent: 100 379 | DesiredCount: 1 380 | LaunchType: "FARGATE" 381 | ServiceRegistries: 382 | - RegistryArn: !GetAtt "ServerRegistry.Arn" 383 | NetworkConfiguration: 384 | AwsvpcConfiguration: 385 | AssignPublicIp: DISABLED 386 | SecurityGroups: 387 | - !Ref SecurityGroup 388 | Subnets: 389 | - Fn::ImportValue: !Sub "${ProjectName}:PrivateSubnet1" 390 | - Fn::ImportValue: !Sub "${ProjectName}:PrivateSubnet2" 391 | TaskDefinition: !Ref ServerTaskDef 392 | 393 | Outputs: 394 | PublicEndpoint: 395 | Description: "Public endpoint for the server" 396 | Value: !Join ["", ["http://", !GetAtt "PublicLoadBalancer.DNSName"]] 397 | Export: 398 | Name: !Sub "${ProjectName}:PublicEndpoint" 399 | -------------------------------------------------------------------------------- /templates/infra.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "Core Stack for Content Streaming Workshop" 3 | Metadata: 4 | LICENSE: MIT 5 | AWS::CloudFormation::Interface: 6 | ParameterGroups: 7 | - Label: 8 | Detault: Debugging (optional) 9 | Parameters: 10 | - EnableBastionHost 11 | - KeyPair 12 | 13 | Parameters: 14 | ProjectName: 15 | Type: String 16 | Description: Project name to link stacks 17 | 18 | EC2Ami: 19 | Type: AWS::SSM::Parameter::Value 20 | Description: EC2 AMI ID 21 | Default: "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2" 22 | 23 | ClusterName: 24 | Type: String 25 | Description: Amazon MSK Cluster Name 26 | 27 | VpcCIDR: 28 | Description: Please enter the IP range (CIDR notation) for this VPC 29 | Type: String 30 | Default: 10.0.0.0/16 31 | 32 | PublicSubnet1CIDR: 33 | Description: Please enter the IP range (CIDR notation) for the public subnet in the first Availability Zone 34 | Type: String 35 | Default: 10.0.0.0/19 36 | 37 | PublicSubnet2CIDR: 38 | Description: Please enter the IP range (CIDR notation) for the public subnet in the second Availability Zone 39 | Type: String 40 | Default: 10.0.32.0/19 41 | 42 | PrivateSubnet1CIDR: 43 | Description: Please enter the IP range (CIDR notation) for the private subnet in the first Availability Zone 44 | Type: String 45 | Default: 10.0.64.0/19 46 | 47 | PrivateSubnet2CIDR: 48 | Description: Please enter the IP range (CIDR notation) for the private subnet in the second Availability Zone 49 | Type: String 50 | Default: 10.0.96.0/19 51 | 52 | # Debugging (optional) 53 | 54 | EnableBastionHost: 55 | AllowedValues: 56 | - "true" 57 | - "false" 58 | Default: "false" 59 | Description: "if true it enables a bastion host for debugging" 60 | Type: String 61 | 62 | KeyPair: 63 | Type: AWS::EC2::KeyPair::KeyName 64 | Description: "EC2 Key Pair to allow SSH access to the bastion host" 65 | 66 | Conditions: 67 | EnableBastionHost: !Equals 68 | - !Ref EnableBastionHost 69 | - "true" 70 | 71 | Resources: 72 | VPC: 73 | Type: AWS::EC2::VPC 74 | Properties: 75 | CidrBlock: !Ref VpcCIDR 76 | EnableDnsHostnames: true 77 | Tags: 78 | - Key: Name 79 | Value: !Ref ProjectName 80 | 81 | InternetGateway: 82 | Type: AWS::EC2::InternetGateway 83 | Properties: 84 | Tags: 85 | - Key: Name 86 | Value: !Ref ProjectName 87 | 88 | InternetGatewayAttachment: 89 | Type: AWS::EC2::VPCGatewayAttachment 90 | Properties: 91 | InternetGatewayId: !Ref InternetGateway 92 | VpcId: !Ref VPC 93 | 94 | PublicSubnet1: 95 | Type: AWS::EC2::Subnet 96 | Properties: 97 | VpcId: !Ref VPC 98 | AvailabilityZone: !Select [0, !GetAZs ""] 99 | CidrBlock: !Ref PublicSubnet1CIDR 100 | MapPublicIpOnLaunch: true 101 | Tags: 102 | - Key: Name 103 | Value: !Sub "${ProjectName} Public Subnet (AZ1)" 104 | 105 | PublicSubnet2: 106 | Type: AWS::EC2::Subnet 107 | Properties: 108 | VpcId: !Ref VPC 109 | AvailabilityZone: !Select [1, !GetAZs ""] 110 | CidrBlock: !Ref PublicSubnet2CIDR 111 | MapPublicIpOnLaunch: true 112 | Tags: 113 | - Key: Name 114 | Value: !Sub "${ProjectName} Public Subnet (AZ2)" 115 | 116 | PrivateSubnet1: 117 | Type: AWS::EC2::Subnet 118 | Properties: 119 | VpcId: !Ref VPC 120 | AvailabilityZone: !Select [0, !GetAZs ""] 121 | CidrBlock: !Ref PrivateSubnet1CIDR 122 | MapPublicIpOnLaunch: false 123 | Tags: 124 | - Key: Name 125 | Value: !Sub "${ProjectName} Private Subnet (AZ1)" 126 | 127 | PrivateSubnet2: 128 | Type: AWS::EC2::Subnet 129 | Properties: 130 | VpcId: !Ref VPC 131 | AvailabilityZone: !Select [1, !GetAZs ""] 132 | CidrBlock: !Ref PrivateSubnet2CIDR 133 | MapPublicIpOnLaunch: false 134 | Tags: 135 | - Key: Name 136 | Value: !Sub "${ProjectName} Private Subnet (AZ2)" 137 | 138 | MSKCluster: 139 | Type: AWS::MSK::Cluster 140 | Properties: 141 | ClusterName: !Ref ClusterName 142 | KafkaVersion: 2.2.1 143 | NumberOfBrokerNodes: 2 144 | EnhancedMonitoring: PER_BROKER 145 | EncryptionInfo: 146 | EncryptionInTransit: 147 | ClientBroker: TLS 148 | InCluster: true 149 | OpenMonitoring: 150 | Prometheus: 151 | JmxExporter: 152 | EnabledInBroker: "true" 153 | NodeExporter: 154 | EnabledInBroker: "true" 155 | BrokerNodeGroupInfo: 156 | BrokerAZDistribution: DEFAULT 157 | InstanceType: kafka.m5.large 158 | SecurityGroups: 159 | - !Ref MSKSecurityGroup 160 | StorageInfo: 161 | EBSStorageInfo: 162 | VolumeSize: 100 163 | ClientSubnets: 164 | - !Ref PrivateSubnet1 165 | - !Ref PrivateSubnet2 166 | 167 | MSKSecurityGroup: 168 | Type: AWS::EC2::SecurityGroup 169 | Properties: 170 | GroupDescription: "Security group for the MSK Cluster" 171 | VpcId: !Ref VPC 172 | SecurityGroupIngress: 173 | - IpProtocol: tcp 174 | FromPort: 2181 175 | ToPort: 2181 176 | CidrIp: !Ref VpcCIDR 177 | - IpProtocol: tcp 178 | FromPort: 9094 179 | ToPort: 9094 180 | CidrIp: !Ref VpcCIDR 181 | - IpProtocol: tcp 182 | FromPort: 9092 183 | ToPort: 9092 184 | CidrIp: !Ref VpcCIDR 185 | 186 | NatGateway1EIP: 187 | Type: AWS::EC2::EIP 188 | DependsOn: InternetGatewayAttachment 189 | Properties: 190 | Domain: vpc 191 | 192 | NatGateway2EIP: 193 | Type: AWS::EC2::EIP 194 | DependsOn: InternetGatewayAttachment 195 | Properties: 196 | Domain: vpc 197 | 198 | NatGateway1: 199 | Type: AWS::EC2::NatGateway 200 | Properties: 201 | AllocationId: !GetAtt NatGateway1EIP.AllocationId 202 | SubnetId: !Ref PublicSubnet1 203 | 204 | NatGateway2: 205 | Type: AWS::EC2::NatGateway 206 | Properties: 207 | AllocationId: !GetAtt NatGateway2EIP.AllocationId 208 | SubnetId: !Ref PublicSubnet2 209 | 210 | PublicRouteTable: 211 | Type: AWS::EC2::RouteTable 212 | Properties: 213 | VpcId: !Ref VPC 214 | Tags: 215 | - Key: Name 216 | Value: !Sub "${ProjectName} Public Routes" 217 | 218 | DefaultPublicRoute: 219 | Type: AWS::EC2::Route 220 | DependsOn: InternetGatewayAttachment 221 | Properties: 222 | RouteTableId: !Ref PublicRouteTable 223 | DestinationCidrBlock: 0.0.0.0/0 224 | GatewayId: !Ref InternetGateway 225 | 226 | PublicSubnet1RouteTableAssociation: 227 | Type: AWS::EC2::SubnetRouteTableAssociation 228 | Properties: 229 | RouteTableId: !Ref PublicRouteTable 230 | SubnetId: !Ref PublicSubnet1 231 | 232 | PublicSubnet2RouteTableAssociation: 233 | Type: AWS::EC2::SubnetRouteTableAssociation 234 | Properties: 235 | RouteTableId: !Ref PublicRouteTable 236 | SubnetId: !Ref PublicSubnet2 237 | 238 | PrivateRouteTable1: 239 | Type: AWS::EC2::RouteTable 240 | Properties: 241 | VpcId: !Ref VPC 242 | Tags: 243 | - Key: Name 244 | Value: !Sub "${ProjectName} Private Routes (AZ1)" 245 | 246 | DefaultPrivateRoute1: 247 | Type: AWS::EC2::Route 248 | Properties: 249 | RouteTableId: !Ref PrivateRouteTable1 250 | DestinationCidrBlock: 0.0.0.0/0 251 | NatGatewayId: !Ref NatGateway1 252 | 253 | PrivateSubnet1RouteTableAssociation: 254 | Type: AWS::EC2::SubnetRouteTableAssociation 255 | Properties: 256 | RouteTableId: !Ref PrivateRouteTable1 257 | SubnetId: !Ref PrivateSubnet1 258 | 259 | PrivateRouteTable2: 260 | Type: AWS::EC2::RouteTable 261 | Properties: 262 | VpcId: !Ref VPC 263 | Tags: 264 | - Key: Name 265 | Value: !Sub "${ProjectName} Private Routes (AZ2)" 266 | 267 | DefaultPrivateRoute2: 268 | Type: AWS::EC2::Route 269 | Properties: 270 | RouteTableId: !Ref PrivateRouteTable2 271 | DestinationCidrBlock: 0.0.0.0/0 272 | NatGatewayId: !Ref NatGateway2 273 | 274 | PrivateSubnet2RouteTableAssociation: 275 | Type: AWS::EC2::SubnetRouteTableAssociation 276 | Properties: 277 | RouteTableId: !Ref PrivateRouteTable2 278 | SubnetId: !Ref PrivateSubnet2 279 | 280 | ECSCluster: 281 | Type: AWS::ECS::Cluster 282 | Properties: 283 | ClusterName: !Ref ProjectName 284 | 285 | CloudMapNamespace: 286 | Type: AWS::ServiceDiscovery::PrivateDnsNamespace 287 | Properties: 288 | Name: !Sub "content.local" 289 | Vpc: !Ref VPC 290 | 291 | BastionSecurityGroup: 292 | Condition: EnableBastionHost 293 | Type: AWS::EC2::SecurityGroup 294 | Properties: 295 | GroupDescription: "Allows SSH access" 296 | VpcId: !Ref VPC 297 | SecurityGroupIngress: 298 | - IpProtocol: tcp 299 | FromPort: 22 300 | ToPort: 22 301 | CidrIp: 0.0.0.0/0 302 | 303 | Bastion: 304 | Condition: EnableBastionHost 305 | Type: AWS::EC2::Instance 306 | Properties: 307 | ImageId: !Ref EC2Ami 308 | InstanceType: t2.micro 309 | KeyName: !Ref KeyPair 310 | SecurityGroupIds: 311 | - !Ref BastionSecurityGroup 312 | SubnetId: !Ref PublicSubnet1 313 | 314 | Outputs: 315 | VPC: 316 | Description: A reference to the created VPC 317 | Value: !Ref VPC 318 | Export: 319 | Name: !Sub "${ProjectName}:VPC" 320 | 321 | PublicSubnet1: 322 | Description: A reference to the public subnet in the 1st Availability Zone 323 | Value: !Ref PublicSubnet1 324 | Export: 325 | Name: !Sub "${ProjectName}:PublicSubnet1" 326 | 327 | PublicSubnet2: 328 | Description: A reference to the public subnet in the 2nd Availability Zone 329 | Value: !Ref PublicSubnet2 330 | Export: 331 | Name: !Sub "${ProjectName}:PublicSubnet2" 332 | 333 | PrivateSubnet1: 334 | Description: A reference to the private subnet in the 1st Availability Zone 335 | Value: !Ref PrivateSubnet1 336 | Export: 337 | Name: !Sub "${ProjectName}:PrivateSubnet1" 338 | 339 | PrivateSubnet2: 340 | Description: A reference to the private subnet in the 2nd Availability Zone 341 | Value: !Ref PrivateSubnet2 342 | Export: 343 | Name: !Sub "${ProjectName}:PrivateSubnet2" 344 | 345 | VpcCIDR: 346 | Description: VPC CIDR 347 | Value: !Ref VpcCIDR 348 | Export: 349 | Name: !Sub "${ProjectName}:VpcCIDR" 350 | 351 | ECSCluster: 352 | Description: A reference to the ECS cluster 353 | Value: !Ref ECSCluster 354 | Export: 355 | Name: !Sub "${ProjectName}:ECSCluster" 356 | 357 | CloudMapNamespaceId: 358 | Description: The id of to the Cloud Map namespace 359 | Value: !GetAtt CloudMapNamespace.Id 360 | Export: 361 | Name: !Sub "${ProjectName}:CloudMapNamespaceId" 362 | 363 | BastionIp: 364 | Condition: EnableBastionHost 365 | Description: The ip address of the bastion host 366 | Value: !GetAtt Bastion.PublicIp 367 | Export: 368 | Name: !Sub "${ProjectName}:BastionIp" 369 | 370 | MSKClusterArn: 371 | Description: The Arn for the MSKMMCluster MSK cluster 372 | Value: !Ref MSKCluster 373 | 374 | MSKSecurityGroupID: 375 | Description: The ID of the security group created for the MSK clusters 376 | Value: !GetAtt 377 | - MSKSecurityGroup 378 | - GroupId 379 | -------------------------------------------------------------------------------- /templates/mesh.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | ProjectName: 3 | Type: String 4 | Description: Project name to link stacks 5 | 6 | Resources: 7 | Mesh: 8 | Type: AWS::AppMesh::Mesh 9 | Properties: 10 | MeshName: !Sub "${ProjectName}-mesh" 11 | 12 | ServerNode: 13 | Type: AWS::AppMesh::VirtualNode 14 | Properties: 15 | MeshName: !GetAtt Mesh.MeshName 16 | VirtualNodeName: server 17 | Spec: 18 | Listeners: 19 | - PortMapping: 20 | Port: 9090 21 | Protocol: grpc 22 | HealthCheck: 23 | HealthyThreshold: 2 24 | IntervalMillis: 5000 25 | TimeoutMillis: 2000 26 | UnhealthyThreshold: 3 27 | Port: 9090 28 | Protocol: grpc 29 | ServiceDiscovery: 30 | AWSCloudMap: 31 | NamespaceName: !Sub "${ProjectName}.local" 32 | ServiceName: server 33 | 34 | VirtualRouter: 35 | Type: AWS::AppMesh::VirtualRouter 36 | Properties: 37 | MeshName: !GetAtt Mesh.MeshName 38 | VirtualRouterName: virtual-router 39 | Spec: 40 | Listeners: 41 | - PortMapping: 42 | Port: 9090 43 | Protocol: grpc 44 | 45 | VirtualService: 46 | DependsOn: 47 | - VirtualRouter 48 | Type: AWS::AppMesh::VirtualService 49 | Properties: 50 | MeshName: !GetAtt Mesh.MeshName 51 | VirtualServiceName: !Sub "server.${ProjectName}.local" 52 | Spec: 53 | Provider: 54 | VirtualRouter: 55 | VirtualRouterName: virtual-router 56 | 57 | Route: 58 | DependsOn: 59 | - VirtualRouter 60 | - ServerNode 61 | Type: AWS::AppMesh::Route 62 | Properties: 63 | MeshName: !GetAtt Mesh.MeshName 64 | VirtualRouterName: virtual-router 65 | RouteName: route 66 | Spec: 67 | GrpcRoute: 68 | Action: 69 | WeightedTargets: 70 | - VirtualNode: server 71 | Weight: 100 72 | Match: 73 | ServiceName: proto.Micro 74 | --------------------------------------------------------------------------------