├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github └── workflows │ ├── go.yaml │ └── release.yaml ├── .vscode └── launch.json ├── README.md ├── conf.sample.yaml ├── docs ├── how-to-add-a-check.md └── img │ └── k8s-api-spec.png ├── go.mod ├── go.sum └── src ├── checker └── checker.go ├── checks └── kubernetes │ ├── deployments │ ├── deployment.go │ └── deployment_test.go │ ├── pods │ ├── pods.go │ └── pods_test.go │ └── services │ ├── service.go │ └── service_test.go └── cmd └── main.go /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.137.0/containers/go/.devcontainer/base.Dockerfile 2 | ARG VARIANT="1" 3 | FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT} 4 | 5 | # [Optional] Install a version of Node.js using nvm for front end dev 6 | ARG INSTALL_NODE="true" 7 | ARG NODE_VERSION="lts/*" 8 | RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi 9 | 10 | # [Optional] Uncomment this section to install additional OS packages. 11 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 12 | # && apt-get -y install --no-install-recommends 13 | 14 | # Install kubectl 15 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 16 | && apt-get install -y apt-transport-https gnupg2 curl 17 | RUN curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add - 18 | RUN echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee -a /etc/apt/sources.list.d/kubernetes.list 19 | RUN sudo apt-get update 20 | RUN sudo apt-get install -y kubectl 21 | 22 | # [Optional] Uncomment the next line to use go get to install anything else you need 23 | # RUN go get -x 24 | 25 | # [Optional] Uncomment this line to install global node packages. 26 | # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.137.0/containers/go 3 | { 4 | "name": "Go", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "args": { 8 | // Update the VARIANT arg to pick a version of Go: 1, 1.15, 1.14 9 | "VARIANT": "1.15", 10 | // Options 11 | "INSTALL_NODE": "false", 12 | "NODE_VERSION": "lts/*" 13 | } 14 | }, 15 | "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ], 16 | 17 | // Set *default* container specific settings.json values on container create. 18 | "settings": { 19 | "terminal.integrated.shell.linux": "/bin/bash", 20 | "go.useGoProxyToCheckForToolUpdates": false, 21 | "go.gopath": "/go", 22 | "go.useLanguageServer": true 23 | }, 24 | 25 | // Add the IDs of extensions you want installed when the container is created. 26 | "extensions": [ 27 | "golang.Go" 28 | ], 29 | 30 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 31 | "forwardPorts": [9000], 32 | 33 | // Use 'postCreateCommand' to run commands after the container is created. 34 | // "postCreateCommand": "go version", 35 | 36 | // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. 37 | "remoteUser": "vscode", 38 | 39 | // https://code.visualstudio.com/docs/remote/containers-advanced#_adding-another-local-file-mount 40 | "mounts": [ 41 | // Mounting local kubeconfig into the container 42 | "source=${localEnv:HOME}/.kube/config,target=/home/vscode/.kube/config,type=bind,consistency=cached" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/go.yaml: -------------------------------------------------------------------------------- 1 | 2 | on: [pull_request] 3 | 4 | jobs: 5 | 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | 11 | - name: Set up Go 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: 1.15 15 | 16 | - name: Build 17 | run: go build -v ./src/cmd/main.go 18 | 19 | - name: Test 20 | run: go test -v ./... 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | # Action source: https://github.com/wangyoucao577/go-release-action 2 | on: 3 | release: 4 | types: [created] 5 | 6 | jobs: 7 | release-linux-amd64: 8 | name: release linux/amd64 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | # build and publish in parallel: linux/386, linux/amd64, windows/386, windows/amd64, darwin/386, darwin/amd64 13 | goos: [linux, windows, darwin] 14 | goarch: ["386", amd64] 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: wangyoucao577/go-release-action@v1.14 18 | with: 19 | github_token: ${{ secrets.GITHUB_TOKEN }} 20 | goos: ${{ matrix.goos }} 21 | goarch: ${{ matrix.goarch }} 22 | goversion: "https://dl.google.com/go/go1.14.14.linux-amd64.tar.gz" 23 | project_path: "./src/cmd" 24 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch Server", 6 | "type": "go", 7 | "request": "launch", 8 | "mode": "debug", 9 | "program": "${workspaceFolder}/src/cmd/main.go", 10 | "env": { 11 | "KSC_CONFIG": "/workspaces/kubernetes-state-checker/conf.sample.yaml" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kubernetes-state-checker 2 | 3 | Kubernetes State Checker as the name suggests helps you check the state of your Kubernetes cluster. 4 | 5 | You might ask, with Kubernetes, don’t you declare the state you want and Kubernetes makes that happen, why do I have to check the state? You would be correct but there are usually interactions from one Kubernetes resource with another or it might depend on an external or third party resource. Anyone of these items might not be able to get into the state you wanted which can have downstream effects or effects on the environment. Even if Kubernetes put the state of the cluster/application in the state you declared, sometimes what you declared could be wrong because someone changed the setting looking to make a fix which was not communicated to downstream dependencies and that could break the environment. 6 | 7 | With a microservice architecture, multiple teams can contribute to one application environment. Ensuring everything coming together and working correctly is often a challenge. There are often the application expert(s) that know how all of these applications integrate with each other and are supposed to work for your environment and this person is relied on to debug integration problems. This is a tedious task and relying on certain people to debug these types of issues makes them a bottleneck. 8 | 9 | ## How Does Kubernetes State Checker help? 10 | 11 | Here is a real example that has been sanitized and made generic. We have an environment with a bunch of microservices. In this scenario, we will talk about 2 of them. Microservice 1 was set to listen on port 5000 and Microservice 2 was set to connect to Microservice 1 on port 30001. Based on the configuration each state of the deployments made it to it’s desired state but when Microservice 2 tried to connect to Microservice 1, that connection failed. 12 | 13 | We just told you the exact problem but when this occurred, it was not apparent that the ports were set incorrectly. The initial thing that alerted us that this environment was now broken was an e2e test that failed. However, the e2e tests only tests from certain entry points into the system and can’t tell us why the system is broken. This led to developers looking at application logs on the various services since they know how the call flows through the system. From the logs, the developers were able to localize the problem to a few microservices but was not able to say exactly why the call is broken. The next step was to loop in the infrastructure people to take a look. The infrastructure people had to catch up on what was happening and with that information started to check various Kubernetes things. After some tedious task of tracing out how the application was configured to what it was trying to communicate with, it was found that the port settings were off and one side of the port numbers would have to change. 14 | 15 | With Kubernetes State Checker, we will be able to declare these states on how the port numbers should be configured and then run a check to make sure it is in that state. If it is not, like in this case, it would tell you that this particular port is not in the state that you said you wanted it in. 16 | This was essentially an integration problem between Microservice 1 and Microservice 2. Microservice 1 was listening on one port but Microservice 2 thought it should connect to Microservice 1 on another port. Who is correct? This is an understanding between the two microservices on how they will connect to each other but nothing is enforcing or checking that. Kubernetes State Checker can be that “check” or “enforcement”. 17 | 18 | 19 | From a developers point of view, this gives them a tool to check the layers underneath the application to ensure that everything in those layers is set to what is expected. 20 | 21 | 22 | From a DevOps/Infrastructure person’s point of view, this allows them to set up an expected state and enable other groups to check for that. It also gives this group a tool where they can run to check the state which can help them eliminate what could possibly be wrong and look at other areas that this tool did not cover. 23 | 24 | ## Use cases 25 | 26 | ### Is an environment setup correctly? 27 | If you had a series of checks that represents a correctly setup environment, then you can use those checks and run it against another environment to make sure everything is in place. If something is not correctly set, then kubernetes-state-checker will output what is not correct. 28 | 29 | ### Feedback loop from dev to production 30 | As the developer is developing the application, this person usually knows what the application needs in order for it to function properly and probably have found some hiccups while debugging this application. The developer can create check(s) very similiar to how they can create unit tests on areas that are known to failures. Then other teams that are managing other environments can uses these check(s) to make sure their environments are correctly setup. 31 | 32 | The check(s) can flow from the other way as well. Usually in a larger organization, the developers might not be the ones that are taking care of the production systems. There might be another team for that. As you run the application(s) in production, you will usually see other operational issues that are not an issue in dev. This team can also write check(s) for these items so that these issue(s) are detected before it becomes a problem. With these checks, the other teams including the developers can run it to make sure everything is good and that the assumptions from development to production are adhere to. 33 | 34 | ## Example usage 35 | 36 | ### Check Kubernetes service port 37 | 38 | ``` 39 | kubernetes-state-checker: 40 | - type: doesServicePortExist 41 | name: Does microservice 1 have a kubernetes service with port 5000 exposed 42 | description: This checks if microservice 1 has a Kubernetes service with port 5000 exposed 43 | namespace: app 44 | # Input values for this specific check 45 | values: 46 | serviceName: microservice-1 47 | port: 5000 48 | ``` 49 | 50 | ### Check environment in a pod 51 | 52 | ``` 53 | kubernetes-state-checker: 54 | - type: doesEnvarExistInDeployment 55 | name: Check that the microservice 2 deployments has the correct envar for microservice 1 56 | description: The microservice 2 uses the "MICROSERVICE_1_HOST_PORT" envar to find microservice 1. This checks to make sure that this envar is there and set to the correct value. 57 | Namespace: app 58 | # Input values for this specific check 59 | values: 60 | deploymentName: microservice-2 61 | envarKey: MICROSERVICE_1_HOST_PORT 62 | envarValue: microservice-1:5000 63 | ``` 64 | 65 | ### Check if a port is open 66 | Maybe even kube exec telnet/nc to test the connection 67 | 68 | 69 | ## Open discussions 70 | 71 | ### A more dynamic way to read configurations 72 | During a peer review of this document, there was an idea put out to see if we can read in configurations in a more dynamic way so that these configurations don't have to be in more than one place. Taking our our microservice 1 and microservice 2 example from above. The actual ports and envars are defined in each services Helm values files. Then with this test, we once again have to define what ports maps to what. This means that the same information is in two places now. When someone wants to update the port for microservice 1, they would have to update it in microservice 1's Helm values and then go into the kubernetes-state-checker's check config yaml and change the value in there as well. This make repetitive and tedious amount of work. 73 | 74 | We would like a way where we can tell this kubernetes-state-checker's check config yaml that here is the port that microservice-1 is listening on and here is the file and here is the envar that microservice-2 is using to reach that port. These values should be the same. 75 | 76 | #### Proposed solutions: 77 | 78 | Be able to read from any yaml file with a `kubernetes-state-checker` section 79 | We can point it to any yaml file and it will find and only the `kubernetes-state-checker` section. 80 | 81 | ``` 82 | fullnameOverride: &name "hos-core-authentication" 83 | 84 | image: 85 | repository: 1234.dkr.ecr.us-west-2.amazonaws.com/hos/hos-core-authentication 86 | pullPolicy: Always 87 | tag: &tag dev 88 | 89 | service: 90 | port: 20004 91 | targetPort: 20004 92 | 93 | some-other-yaml: 94 | foo: bar 95 | 96 | kubernetes-state-checker: 97 | - type: doesServicePortExist 98 | name: Does hos-core-authentication have a kubernetes service with port 20004 exposed 99 | description: This checks if hos-core-authentication has a Kubernetes service with port 20004 exposed 100 | namespace: app 101 | # Input values for this specific check 102 | values: 103 | serviceName: hos-core-authentication 104 | port: 20004 105 | ``` 106 | 107 | It will ignore all sections and just use the information in the `kubernetes-state-checker` section. 108 | 109 | # Running this in VScode 110 | This has the configuration files that helps you bring up the enviroment you need to develop against this locally via a Docker container. 111 | 112 | This is based on this example: https://github.com/microsoft/vscode-remote-try-go 113 | 114 | You will have to install the `Visual Studio Code Remote - Containers extension`: https://code.visualstudio.com/docs/remote/containers#_getting-started 115 | 116 | Once you install this, and restart VScode in this repository, it will ask if you want to open it in a container. The first time you do this, it will take some time since it is building the container based on the Dockerfile in the `.devcontainer` directory. After it builds the container, it will open this project inside of this container and you can code away as normal. 117 | 118 | Install the go dependencies: 119 | ``` 120 | go get gopkg.in/yaml.v2 121 | go get k8s.io/apimachinery/pkg/api/errors 122 | go get k8s.io/apimachinery/pkg/apis/meta/v1 123 | go get k8s.io/client-go/kubernetes 124 | go get k8s.io/client-go/tools/clientcmd 125 | go get k8s.io/client-go/util/homedir 126 | go get github.com/evanphx/json-patch 127 | go get k8s.io/kube-openapi/pkg/util/proto 128 | go get github.com/olekukonko/tablewriter 129 | ``` 130 | ## Set go path 131 | ``` 132 | export GOPATH=$GOPATH:$PWD 133 | ``` 134 | 135 | ## Run all unit tests 136 | ``` 137 | go test ./... 138 | ``` 139 | 140 | ## Kubeconfig 141 | The `.devcontainer/devcontainer.json` file specifies a local mount from your `$HOME/.kube/config` into the `$HOME/.kube/config` inside the container. 142 | -------------------------------------------------------------------------------- /conf.sample.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | some: other configs 3 | random: 5 4 | 5 | kubernetes-state-checker: 6 | - ttype: serviceChecks 7 | name: Checks for various aspects of a service 8 | description: Allows you to check for various aspects of a service 9 | namespace: hos-m2 10 | # Input values for this specific check 11 | values: 12 | # The service name to act on 13 | serviceName: healthapp-caregaps 14 | # Port number to check if the `ports` check is enabled 15 | port: 20014 16 | checksEnabled: 17 | # Check if there is a cluster IP associated with this endpoint 18 | clusterIP: true 19 | # Checks if there are endpoints associated to this service or not 20 | endpoints: true 21 | # Check if host port is enabled or not 22 | hostPort: false 23 | # Check if the port is the one that is specified above 24 | ports: true 25 | - ttype: deploymentChecks 26 | name: Checks for various aspects of a deployment 27 | description: Allows you to check for various aspects of a deployment 28 | namespace: hos-m2 29 | # Input values for this specific check 30 | values: 31 | # The service name to act on 32 | deploymentName: healthapp-caregaps 33 | checksEnabled: 34 | containers: 35 | - name: healthapp-caregaps-app 36 | # Check if a set of environment variables are present 37 | env: 38 | - name: AUTH_ADDRESS 39 | value: localhost:30004 40 | # image: 41 | # name: "something*:*" 42 | # healthcheck: 43 | # value: something 44 | 45 | # Check if a configmap mount is set 46 | configMapMounts: 47 | - foo 48 | - bar 49 | - ttype: deploymentChecks 50 | name: Checks that the deployment has 3 contianers 51 | description: Allows you to check for various aspects of a deployment 52 | namespace: hos-m2 53 | # Input values for this specific check 54 | values: 55 | # The service name to act on 56 | deploymentName: healthapp-caregaps 57 | checksEnabled: 58 | containers: 59 | - name: healthapp-caregaps-app 60 | - name: envoy 61 | - name: opa 62 | - ttype: podChecks 63 | name: Checks that the pod is in a running state 64 | description: Allows you to check for various aspects of a pod 65 | namespace: hos-m2 66 | # Input values for this specific check 67 | values: 68 | checksEnabled: 69 | state: 70 | # The pod name to act on. This is a regex field. 71 | - podName: healthapp-caregaps-* 72 | desiredState: Running 73 | - podName: foo 74 | desiredState: CrashLoopBackOff 75 | -------------------------------------------------------------------------------- /docs/how-to-add-a-check.md: -------------------------------------------------------------------------------- 1 | How to add a check 2 | =================== 3 | This document walks you through on how to add a check into this application. 4 | 5 | This will use the `deploymentChecks` check as an example. 6 | 7 | ## Create the input data structure 8 | The input data structure is the yaml that the user of this check will input into the application and the application will use this information to perform the check. 9 | 10 | This is the entire check information 11 | 12 | 13 | ```yaml 14 | - ttype: deploymentChecks 15 | name: Checks for various aspects of a deployment 16 | description: Allows you to check for various aspects of a deployment 17 | namespace: hos-m2 18 | # Input values for this specific check 19 | values: 20 | # The service name to act on 21 | deploymentName: healthapp-caregaps 22 | checksEnabled: 23 | # Check if a set of environment variables are present 24 | environmentVars: 25 | # Specify the container in the pod that should contain this set of envars 26 | containers: 27 | # A specific named container 28 | - name: container1 29 | envars: 30 | - foo: bar 31 | - foo2: bar2 32 | # A wildcard to find this envar in any container in the deployment 33 | - name: "*" 34 | envars: 35 | - foo: bar 36 | - foo2: bar2 37 | # Check if a configmap mount is set 38 | configMapMounts: 39 | - foo 40 | - bar 41 | ``` 42 | 43 | This section is mandatory and the structure of it cannot be changed: 44 | ```yaml 45 | - ttype: deploymentChecks 46 | name: Checks for various aspects of a deployment 47 | description: Allows you to check for various aspects of a deployment 48 | namespace: hos-m2 49 | # Input values for this specific check 50 | values: {} 51 | ``` 52 | 53 | The section under the `values: {}` is unique for all checks. The `values` section is all unique to the check itself depending on what type of input it requires from the user to perform the check. 54 | 55 | ## Add new check to `checker.go` 56 | In the file `checker.go`, there is a function `Run()`. This function holds a list of the checks and executes it. You will have to add your new check function into the `switch` statement. 57 | 58 | ## Adding Check files 59 | You can mostly do TDD here with a little caveat. I find the k8s API data structure to be pretty complex and confusing. There are v1, betas, extentions and I don't know it well enough or really want to remember the entire stucture on where things live. The way I am doing this right now is to scaffold the check enough so that I can run the app and get back the response for the k8s resource I want. Then from there I can put vscode into debug mode and just look at the data structure and construct my unit tests k8s fake data structure so that I can make a unit test. 60 | 61 | Here is what I do (which is just easy for me). Feel free to do or suggest another method. 62 | 63 | Ill add in the deployment check file: `src/checks/kubernetes/deployments/deployment.go` 64 | 65 | I will scaffold this to a point where I can get back the k8s deployment object. Basically get it to run and query the k8s api for info: 66 | 67 | ```go 68 | deployment, err := kubeClientSet.AppsV1().Deployments(i.namespace).List(context.TODO(), metav1.ListOptions{}) 69 | if err != nil { 70 | panic(err.Error()) 71 | } 72 | ``` 73 | 74 | You can also take a look at this commit for an example: https://github.com/anthem-ai/kubernetes-state-checker/commit/a80bbb1bcfebd0140fc05c0e1b52125550481cba#diff-54c8b8aaa53e128cb37fffc9f8dac55169dd1aef27d8800b0ab9ebe2ffc5eaf0R79 75 | 76 | If you run this in the debugger and put the break point to after this section runs, you can see the entire data structure of the k8s resource and it's type: 77 | 78 | ![k8s api spec](./img/k8s-api-spec.png "k8s-api-spec") 79 | 80 | This made it a lot easier for me when constructing the unit test. 81 | 82 | ## Adding the unit test 83 | Vscode can help you add in the unit test and get you going. Open the `deployment.go` file or the file you want to generate a unit test file for. Then press `F1` then select or type in: `Go: Generate Unit Test For File`. A new file for the file you were just on will be created with the unit tests in it. It will create a unit test for every funtion in the source file. 84 | 85 | For the deployment, we are mostly interested in the `Test_inputs_GeneralCheck` unit test. 86 | 87 | The simpliest thing you can do is to just add in the input variables and the expected output for this function. There is a comment in each unit test `// TODO: Add test cases.`. This is where you will add in the input and expected outputs. 88 | 89 | Refer to what is currently in this project for further expamples on how to add in the data structure. 90 | 91 | Now you can fill out the k8s fake resource and what you expect the output to be. Then you can do a TDD workflow when developing this new check. 92 | -------------------------------------------------------------------------------- /docs/img/k8s-api-spec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthem-ai/kubernetes-state-checker/d698c0fa59b7821a4e23e166836880879d6cc592/docs/img/k8s-api-spec.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/anthem-ai/kubernetes-state-checker 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/imdario/mergo v0.3.11 // indirect 7 | github.com/olekukonko/tablewriter v0.0.4 8 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect 9 | gopkg.in/yaml.v2 v2.3.0 10 | k8s.io/api v0.20.2 11 | k8s.io/apimachinery v0.20.2 12 | k8s.io/client-go v0.20.2 13 | k8s.io/utils v0.0.0-20210111153108-fddb29f9d009 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 13 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 14 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 15 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 16 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 17 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 18 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 19 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 20 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 21 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 22 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 23 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 24 | github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 25 | github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= 26 | github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= 27 | github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= 28 | github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= 29 | github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= 30 | github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= 31 | github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= 32 | github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= 33 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 34 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 35 | github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= 36 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 37 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 38 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= 39 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 40 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 41 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 42 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 43 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 44 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 45 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 46 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 47 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 48 | github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= 49 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 50 | github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 51 | github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 52 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 53 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 54 | github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= 55 | github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 56 | github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= 57 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 58 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 59 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 60 | github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 61 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 62 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 63 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 64 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 65 | github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY= 66 | github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= 67 | github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= 68 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 69 | github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= 70 | github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= 71 | github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= 72 | github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 73 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 74 | github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= 75 | github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 76 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 77 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 78 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 79 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 80 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 81 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 82 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 83 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 84 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 85 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 86 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 87 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 88 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 89 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 90 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 91 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 92 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 93 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 94 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 95 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 96 | github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= 97 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 98 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 99 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 100 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 101 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 102 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 103 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 104 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 105 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= 106 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 107 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 108 | github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= 109 | github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 110 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 111 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 112 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 113 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 114 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 115 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 116 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 117 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 118 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 119 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 120 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 121 | github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I= 122 | github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= 123 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 124 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 125 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 126 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 127 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 128 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 129 | github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 130 | github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= 131 | github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 132 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 133 | github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= 134 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 135 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 136 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 137 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 138 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 139 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 140 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 141 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 142 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 143 | github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= 144 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 145 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 146 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 147 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 148 | github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= 149 | github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 150 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 151 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 152 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 153 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 154 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 155 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 156 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 157 | github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 158 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 159 | github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= 160 | github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= 161 | github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 162 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 163 | github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= 164 | github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 165 | github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 166 | github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= 167 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 168 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 169 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 170 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 171 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 172 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 173 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 174 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 175 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 176 | github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 177 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 178 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 179 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 180 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 181 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 182 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 183 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 184 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 185 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 186 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 187 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 188 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 189 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 190 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 191 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 192 | golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 193 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 194 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 195 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 196 | golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE= 197 | golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 198 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 199 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 200 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 201 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 202 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 203 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 204 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 205 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 206 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 207 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 208 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 209 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 210 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 211 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 212 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 213 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 214 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 215 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 216 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 217 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 218 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 219 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 220 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 221 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 222 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 223 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 224 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 225 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 226 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 227 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 228 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 229 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 230 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 231 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 232 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 233 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 234 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 235 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 236 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 237 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 238 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 239 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 240 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 241 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 242 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 243 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 244 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 245 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 246 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 247 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 248 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= 249 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 250 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= 251 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 252 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 253 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 254 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 255 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= 256 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 257 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 258 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 259 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 260 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 261 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 262 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 263 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 264 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 265 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 266 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 267 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 268 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 269 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 270 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 271 | golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 272 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 273 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 274 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 275 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 276 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 277 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 278 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 279 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 280 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 281 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 282 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 283 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 284 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 285 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 286 | golang.org/x/sys v0.0.0-20201112073958-5cba982894dd h1:5CtCZbICpIOFdgO940moixOPjc0178IU44m4EjOO5IY= 287 | golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 288 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 289 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 290 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 291 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 292 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 293 | golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= 294 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 295 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 296 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 297 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 298 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 299 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE= 300 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 301 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 302 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 303 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 304 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 305 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 306 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 307 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 308 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 309 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 310 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 311 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 312 | golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 313 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 314 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 315 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 316 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 317 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 318 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 319 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 320 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 321 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 322 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 323 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 324 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 325 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 326 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 327 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 328 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 329 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 330 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 331 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 332 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 333 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 334 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 335 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 336 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 337 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 338 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 339 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 340 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 341 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 342 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 343 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 344 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 345 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 346 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 347 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 348 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 349 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 350 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 351 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 352 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 353 | google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= 354 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 355 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 356 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 357 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 358 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 359 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 360 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 361 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 362 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 363 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 364 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 365 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 366 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 367 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 368 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 369 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 370 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 371 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 372 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 373 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 374 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 375 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 376 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 377 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 378 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 379 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 380 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 381 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 382 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 383 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 384 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 385 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 386 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 387 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 388 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 389 | google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= 390 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 391 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 392 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 393 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 394 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 395 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 396 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 397 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 398 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 399 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 400 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 401 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 402 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 403 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 404 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 405 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 406 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 407 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 408 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 409 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 410 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 411 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 412 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 413 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 414 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 415 | k8s.io/api v0.20.2 h1:y/HR22XDZY3pniu9hIFDLpUCPq2w5eQ6aV/VFQ7uJMw= 416 | k8s.io/api v0.20.2/go.mod h1:d7n6Ehyzx+S+cE3VhTGfVNNqtGc/oL9DCdYYahlurV8= 417 | k8s.io/apimachinery v0.20.2 h1:hFx6Sbt1oG0n6DZ+g4bFt5f6BoMkOjKWsQFu077M3Vg= 418 | k8s.io/apimachinery v0.20.2/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= 419 | k8s.io/client-go v0.20.2 h1:uuf+iIAbfnCSw8IGAv/Rg0giM+2bOzHLOsbbrwrdhNQ= 420 | k8s.io/client-go v0.20.2/go.mod h1:kH5brqWqp7HDxUFKoEgiI4v8G1xzbe9giaCenUWJzgE= 421 | k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= 422 | k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= 423 | k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ= 424 | k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= 425 | k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd h1:sOHNzJIkytDF6qadMNKhhDRpc6ODik8lVC6nOur7B2c= 426 | k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= 427 | k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= 428 | k8s.io/utils v0.0.0-20210111153108-fddb29f9d009 h1:0T5IaWHO3sJTEmCP6mUlBvMukxPKUQWqiI/YuiBNMiQ= 429 | k8s.io/utils v0.0.0-20210111153108-fddb29f9d009/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= 430 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 431 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 432 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 433 | sigs.k8s.io/structured-merge-diff/v4 v4.0.2 h1:YHQV7Dajm86OuqnIR6zAelnDWBRjo+YhYV9PmGrh1s8= 434 | sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= 435 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 436 | sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= 437 | sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= 438 | -------------------------------------------------------------------------------- /src/checker/checker.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | import ( 4 | "github.com/anthem-ai/kubernetes-state-checker/src/checks/kubernetes/deployments" 5 | "github.com/anthem-ai/kubernetes-state-checker/src/checks/kubernetes/pods" 6 | "github.com/anthem-ai/kubernetes-state-checker/src/checks/kubernetes/services" 7 | 8 | "k8s.io/client-go/kubernetes" 9 | ) 10 | 11 | var kubeClientSet *kubernetes.Clientset 12 | 13 | type Check struct { 14 | valuesYaml string `yaml:"checkYaml"` 15 | Ttype string `yaml:"ttype"` 16 | Name string `yaml:"name"` 17 | Description string `yaml:"description"` 18 | Namespace string `yaml:"namespace"` 19 | Values interface{} `yaml:"values"` 20 | } 21 | 22 | type results struct { 23 | DidPass bool 24 | Message string 25 | } 26 | 27 | // New New 28 | func New(valuesYaml string, clientSet *kubernetes.Clientset, ttype string, name string, description string, namespace string, values interface{}) Check { 29 | c := Check{valuesYaml, ttype, name, description, namespace, values} 30 | kubeClientSet = clientSet 31 | return c 32 | } 33 | 34 | // Run - runner 35 | func (c Check) Run() results { 36 | 37 | var returnResults results 38 | 39 | switch c.Ttype { 40 | 41 | case "serviceChecks": 42 | check := services.New(c.valuesYaml, c.Name, c.Namespace) 43 | r := check.GeneralCheck(kubeClientSet) 44 | 45 | returnResults = results{ 46 | DidPass: r.DidPass, 47 | Message: r.Message, 48 | } 49 | case "deploymentChecks": 50 | check := deployments.New(c.valuesYaml, c.Name, c.Namespace) 51 | r := check.GeneralCheck(kubeClientSet) 52 | 53 | returnResults = results{ 54 | DidPass: r.DidPass, 55 | Message: r.Message, 56 | } 57 | case "podChecks": 58 | check := pods.New(c.valuesYaml, c.Name, c.Namespace) 59 | r := check.GeneralCheck(kubeClientSet) 60 | 61 | returnResults = results{ 62 | DidPass: r.DidPass, 63 | Message: r.Message, 64 | } 65 | } 66 | 67 | return returnResults 68 | } 69 | -------------------------------------------------------------------------------- /src/checks/kubernetes/deployments/deployment.go: -------------------------------------------------------------------------------- 1 | package deployments 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "gopkg.in/yaml.v2" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/client-go/kubernetes" 11 | ) 12 | 13 | type inputs struct { 14 | valuesYaml string 15 | checkName string 16 | namespace string 17 | } 18 | 19 | type Results struct { 20 | DidPass bool 21 | Message string 22 | } 23 | 24 | // New New 25 | func New(valuesYaml string, checkName string, namespace string) inputs { 26 | s := inputs{valuesYaml, checkName, namespace} 27 | 28 | return s 29 | } 30 | 31 | type deploymentStruct struct { 32 | Values struct { 33 | DeploymentName string `yaml:"deploymentName"` 34 | ChecksEnabled struct { 35 | Containers []struct { 36 | Name string `yaml:"name"` 37 | Env []struct { 38 | Name string `yaml:"name,omitempty"` 39 | Value string `yaml:"value,omitempty"` 40 | } `yaml:"env,omitempty"` 41 | } `yaml:"containers,omitempty"` 42 | } `yaml:"checksEnabled"` 43 | } `yaml:"values"` 44 | } 45 | 46 | func deploymentParse(valuesYaml string, v *deploymentStruct) error { 47 | 48 | err := yaml.Unmarshal([]byte(valuesYaml), &v) 49 | if err != nil { 50 | return errors.New(fmt.Sprintf("YAML Parse Error: %v", err)) 51 | } 52 | 53 | if v.Values.DeploymentName == "" { 54 | return errors.New("Check values: no `DeploymentName` set") 55 | } 56 | 57 | return nil 58 | } 59 | 60 | // GeneralCheck GeneralCheck 61 | func (i inputs) GeneralCheck(kubeClientSet kubernetes.Interface) Results { 62 | 63 | var values deploymentStruct 64 | 65 | // Set initial check results 66 | checkResult := Results{ 67 | DidPass: false, 68 | Message: "", 69 | } 70 | 71 | didValuesParse := false 72 | 73 | err := deploymentParse(i.valuesYaml, &values) 74 | if err != nil { 75 | didValuesParse = true 76 | checkResult.Message = fmt.Sprintf("%v", err) 77 | } 78 | 79 | if !didValuesParse { 80 | 81 | // Get data from kubernetes 82 | deployment, err := kubeClientSet.AppsV1().Deployments(i.namespace).List(context.TODO(), metav1.ListOptions{}) 83 | if err != nil { 84 | panic(err.Error()) 85 | } 86 | 87 | // Loop through all of the services found 88 | for _, aDeployment := range deployment.Items { 89 | 90 | // Find the deployment we want to look at 91 | if aDeployment.ObjectMeta.Name == values.Values.DeploymentName { 92 | 93 | // 94 | // Check for envars 95 | // 96 | // Number of containers to check 97 | numberOfContainers := len(values.Values.ChecksEnabled.Containers) 98 | numberOfContainersEnvarsFound := 0 99 | 100 | // Loop through the containers in the input values 101 | for _, inputContainer := range values.Values.ChecksEnabled.Containers { 102 | 103 | // Find the container in the Deployment 104 | for _, container := range aDeployment.Spec.Template.Spec.Containers { 105 | if inputContainer.Name == container.Name { 106 | 107 | // The number of envars that should exist 108 | numberOfEnvars := len(inputContainer.Env) 109 | numberOfEnvarsFound := 0 110 | 111 | // Find the envars in the k8s pod's containers 112 | for _, inputContainerEnv := range inputContainer.Env { 113 | for _, k8sDeploymentEnv := range container.Env { 114 | if inputContainerEnv.Name == k8sDeploymentEnv.Name && 115 | inputContainerEnv.Value == k8sDeploymentEnv.Value { 116 | // Found the envar 117 | numberOfEnvarsFound++ 118 | } 119 | } 120 | } 121 | 122 | if numberOfEnvars > 0 { 123 | if numberOfEnvars == numberOfEnvarsFound { 124 | // Found the correct amount of envars 125 | numberOfContainersEnvarsFound++ 126 | checkResult.Message += "* Found all envars in Deployment: " + values.Values.DeploymentName + " | container: " + container.Name + "\n" 127 | } 128 | } 129 | } 130 | } 131 | } 132 | 133 | if numberOfContainers == numberOfContainersEnvarsFound { 134 | // Found the envars in all of the input check's envar(s) 135 | checkResult.DidPass = true 136 | } else { 137 | checkResult.DidPass = false 138 | } 139 | 140 | // 141 | // Check for the the containers that has the `containerMustBePresent` flag set to true 142 | // 143 | if len(values.Values.ChecksEnabled.Containers) > 0 { 144 | 145 | didFindAllContainers := true 146 | 147 | // Find each container in the deployment based on the user input 148 | for _, inputContainer := range values.Values.ChecksEnabled.Containers { 149 | 150 | didFindContainer := false 151 | 152 | // Search for the user inputted container in the deployment 153 | for _, container := range aDeployment.Spec.Template.Spec.Containers { 154 | if inputContainer.Name == container.Name { 155 | didFindContainer = true 156 | } 157 | } 158 | 159 | if !didFindContainer { 160 | didFindAllContainers = false 161 | } 162 | } 163 | 164 | if didFindAllContainers { 165 | checkResult.DidPass = true 166 | checkResult.Message += "* Found the correct number of containers in this deployment\n" 167 | } else { 168 | checkResult.DidPass = false 169 | } 170 | } 171 | } 172 | } 173 | 174 | } 175 | 176 | return checkResult 177 | } 178 | -------------------------------------------------------------------------------- /src/checks/kubernetes/deployments/deployment_test.go: -------------------------------------------------------------------------------- 1 | package deployments 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | appsv1 "k8s.io/api/apps/v1" 8 | corev1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/client-go/kubernetes" 11 | "k8s.io/client-go/kubernetes/fake" 12 | ) 13 | 14 | func TestNew(t *testing.T) { 15 | type args struct { 16 | valuesYaml string 17 | checkName string 18 | namespace string 19 | } 20 | tests := []struct { 21 | name string 22 | args args 23 | want inputs 24 | }{ 25 | // TODO: Add test cases. 26 | } 27 | for _, tt := range tests { 28 | t.Run(tt.name, func(t *testing.T) { 29 | if got := New(tt.args.valuesYaml, tt.args.checkName, tt.args.namespace); !reflect.DeepEqual(got, tt.want) { 30 | t.Errorf("New() = %v, want %v", got, tt.want) 31 | } 32 | }) 33 | } 34 | } 35 | 36 | func Test_deploymentParse(t *testing.T) { 37 | type args struct { 38 | valuesYaml string 39 | v *deploymentStruct 40 | } 41 | tests := []struct { 42 | name string 43 | args args 44 | wantErr bool 45 | }{ 46 | // TODO: Add test cases. 47 | } 48 | for _, tt := range tests { 49 | t.Run(tt.name, func(t *testing.T) { 50 | if err := deploymentParse(tt.args.valuesYaml, tt.args.v); (err != nil) != tt.wantErr { 51 | t.Errorf("deploymentParse() error = %v, wantErr %v", err, tt.wantErr) 52 | } 53 | }) 54 | } 55 | } 56 | 57 | func Test_inputs_GeneralCheck(t *testing.T) { 58 | type fields struct { 59 | valuesYaml string 60 | checkName string 61 | namespace string 62 | } 63 | type args struct { 64 | kubeClientSet kubernetes.Interface 65 | } 66 | tests := []struct { 67 | name string 68 | fields fields 69 | args args 70 | want Results 71 | }{ 72 | // TODO: Add test cases. 73 | { 74 | name: "Checking deployment envars (positive)", 75 | fields: fields{ 76 | checkName: "check1", 77 | namespace: "ns1", 78 | // The spacing is real finicky. yaml can't have tabs. All spacing must be spaces 79 | valuesYaml: `--- 80 | values: 81 | # The service name to act on 82 | deploymentName: deployment1 83 | checksEnabled: 84 | containers: 85 | - name: container1 86 | env: 87 | - name: foo 88 | value: bar 89 | - name: foo2 90 | value: bar2 91 | - name: container2 92 | env: 93 | - name: foo 94 | value: bar`, 95 | }, 96 | args: args{ 97 | // Doc/example: https://gianarb.it/blog/unit-testing-kubernetes-client-in-go 98 | kubeClientSet: fake.NewSimpleClientset(&appsv1.DeploymentList{ 99 | Items: []appsv1.Deployment{ 100 | { 101 | ObjectMeta: metav1.ObjectMeta{ 102 | Name: "deployment1", 103 | Namespace: "ns1", 104 | Annotations: map[string]string{}, 105 | }, 106 | Spec: appsv1.DeploymentSpec{ 107 | Template: corev1.PodTemplateSpec{ 108 | Spec: corev1.PodSpec{ 109 | Containers: []corev1.Container{ 110 | { 111 | Name: "container1", 112 | Env: []corev1.EnvVar{ 113 | { 114 | Name: "foo", 115 | Value: "bar", 116 | }, 117 | { 118 | Name: "foo2", 119 | Value: "bar2", 120 | }, 121 | { 122 | Name: "foo3", 123 | Value: "bar3", 124 | }, 125 | }, 126 | }, 127 | { 128 | Name: "container2", 129 | Env: []corev1.EnvVar{ 130 | { 131 | Name: "foo", 132 | Value: "bar", 133 | }, 134 | }, 135 | }, 136 | }, 137 | }, 138 | }, 139 | }, 140 | }, 141 | }, 142 | }), 143 | }, 144 | want: Results{ 145 | DidPass: true, 146 | Message: `* Found all envars in Deployment: deployment1 | container: container1 147 | * Found all envars in Deployment: deployment1 | container: container2 148 | * Found the correct number of containers in this deployment 149 | `, 150 | }, 151 | }, 152 | { 153 | name: "Checking the number of pods in a deployment (positive)", 154 | fields: fields{ 155 | checkName: "check1", 156 | namespace: "ns1", 157 | // The spacing is real finicky. yaml can't have tabs. All spacing must be spaces 158 | valuesYaml: `--- 159 | values: 160 | # The service name to act on 161 | deploymentName: check-number-of-pods 162 | checksEnabled: 163 | containers: 164 | - name: pod-container1 165 | - name: pod-container2`, 166 | }, 167 | args: args{ 168 | // Doc/example: https://gianarb.it/blog/unit-testing-kubernetes-client-in-go 169 | kubeClientSet: fake.NewSimpleClientset(&appsv1.DeploymentList{ 170 | Items: []appsv1.Deployment{ 171 | { 172 | ObjectMeta: metav1.ObjectMeta{ 173 | Name: "check-number-of-pods", 174 | Namespace: "ns1", 175 | Annotations: map[string]string{}, 176 | }, 177 | Spec: appsv1.DeploymentSpec{ 178 | Template: corev1.PodTemplateSpec{ 179 | Spec: corev1.PodSpec{ 180 | Containers: []corev1.Container{ 181 | { 182 | Name: "pod-container1", 183 | Env: []corev1.EnvVar{ 184 | { 185 | Name: "pod", 186 | Value: "bar", 187 | }, 188 | { 189 | Name: "pod", 190 | Value: "bar2", 191 | }, 192 | }, 193 | }, 194 | { 195 | Name: "pod-container2", 196 | Env: []corev1.EnvVar{ 197 | { 198 | Name: "pod", 199 | Value: "bar", 200 | }, 201 | }, 202 | }, 203 | }, 204 | }, 205 | }, 206 | }, 207 | }, 208 | }, 209 | }), 210 | }, 211 | want: Results{ 212 | DidPass: true, 213 | Message: `* Found the correct number of containers in this deployment 214 | `, 215 | }, 216 | }, 217 | { 218 | name: "Checking the number of pods in a deployment 2 (positive)", 219 | fields: fields{ 220 | checkName: "check1", 221 | namespace: "ns1", 222 | // The spacing is real finicky. yaml can't have tabs. All spacing must be spaces 223 | valuesYaml: `--- 224 | values: 225 | # The service name to act on 226 | deploymentName: check-number-of-pods 227 | checksEnabled: 228 | containers: 229 | - name: pod-container1 230 | - name: pod-container2`, 231 | }, 232 | args: args{ 233 | // Doc/example: https://gianarb.it/blog/unit-testing-kubernetes-client-in-go 234 | kubeClientSet: fake.NewSimpleClientset(&appsv1.DeploymentList{ 235 | Items: []appsv1.Deployment{ 236 | { 237 | ObjectMeta: metav1.ObjectMeta{ 238 | Name: "check-number-of-pods", 239 | Namespace: "ns1", 240 | Annotations: map[string]string{}, 241 | }, 242 | Spec: appsv1.DeploymentSpec{ 243 | Template: corev1.PodTemplateSpec{ 244 | Spec: corev1.PodSpec{ 245 | Containers: []corev1.Container{ 246 | { 247 | Name: "pod-container1", 248 | Env: []corev1.EnvVar{ 249 | { 250 | Name: "pod", 251 | Value: "bar", 252 | }, 253 | { 254 | Name: "pod", 255 | Value: "bar2", 256 | }, 257 | }, 258 | }, 259 | { 260 | Name: "pod-container2", 261 | Env: []corev1.EnvVar{ 262 | { 263 | Name: "pod", 264 | Value: "bar", 265 | }, 266 | }, 267 | }, 268 | // THis test has more pods in the deployments than what the user is looking for 269 | { 270 | Name: "pod-container3", 271 | Env: []corev1.EnvVar{ 272 | { 273 | Name: "pod", 274 | Value: "bar", 275 | }, 276 | }, 277 | }, 278 | }, 279 | }, 280 | }, 281 | }, 282 | }, 283 | }, 284 | }), 285 | }, 286 | want: Results{ 287 | DidPass: true, 288 | Message: `* Found the correct number of containers in this deployment 289 | `, 290 | }, 291 | }, 292 | { 293 | name: "Checking the number of pods in a deployment 1 (negative)", 294 | fields: fields{ 295 | checkName: "check1", 296 | namespace: "ns1", 297 | // The spacing is real finicky. yaml can't have tabs. All spacing must be spaces 298 | valuesYaml: `--- 299 | values: 300 | # The service name to act on 301 | deploymentName: check-number-of-pods 302 | checksEnabled: 303 | containers: 304 | - name: pod-container1 305 | - name: pod-container2`, 306 | }, 307 | args: args{ 308 | // Doc/example: https://gianarb.it/blog/unit-testing-kubernetes-client-in-go 309 | kubeClientSet: fake.NewSimpleClientset(&appsv1.DeploymentList{ 310 | Items: []appsv1.Deployment{ 311 | { 312 | ObjectMeta: metav1.ObjectMeta{ 313 | Name: "check-number-of-pods", 314 | Namespace: "ns1", 315 | Annotations: map[string]string{}, 316 | }, 317 | Spec: appsv1.DeploymentSpec{ 318 | Template: corev1.PodTemplateSpec{ 319 | Spec: corev1.PodSpec{ 320 | Containers: []corev1.Container{ 321 | { 322 | Name: "pod-container1", 323 | Env: []corev1.EnvVar{ 324 | { 325 | Name: "pod", 326 | Value: "bar", 327 | }, 328 | { 329 | Name: "pod", 330 | Value: "bar2", 331 | }, 332 | }, 333 | }, 334 | }, 335 | }, 336 | }, 337 | }, 338 | }, 339 | }, 340 | }), 341 | }, 342 | want: Results{ 343 | DidPass: false, 344 | Message: "", 345 | }, 346 | }, 347 | } 348 | for _, tt := range tests { 349 | t.Run(tt.name, func(t *testing.T) { 350 | i := inputs{ 351 | valuesYaml: tt.fields.valuesYaml, 352 | checkName: tt.fields.checkName, 353 | namespace: tt.fields.namespace, 354 | } 355 | if got := i.GeneralCheck(tt.args.kubeClientSet); !reflect.DeepEqual(got, tt.want) { 356 | t.Errorf("inputs.GeneralCheck() = %v, want %v", got, tt.want) 357 | } 358 | }) 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /src/checks/kubernetes/pods/pods.go: -------------------------------------------------------------------------------- 1 | package pods 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "regexp" 8 | 9 | "gopkg.in/yaml.v2" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/client-go/kubernetes" 12 | ) 13 | 14 | type inputs struct { 15 | valuesYaml string 16 | checkName string 17 | namespace string 18 | } 19 | 20 | type Results struct { 21 | DidPass bool 22 | Message string 23 | } 24 | 25 | // New New 26 | func New(valuesYaml string, checkName string, namespace string) inputs { 27 | s := inputs{valuesYaml, checkName, namespace} 28 | 29 | return s 30 | } 31 | 32 | type podStruct struct { 33 | Values struct { 34 | ChecksEnabled struct { 35 | State []struct { 36 | PodName string `yaml:"podName"` 37 | DesiredState string `yaml:"desiredState"` 38 | } `yaml:"state,omitempty"` 39 | } `yaml:"checksEnabled"` 40 | } `yaml:"values"` 41 | } 42 | 43 | func podParse(valuesYaml string, v *podStruct) error { 44 | 45 | err := yaml.Unmarshal([]byte(valuesYaml), &v) 46 | if err != nil { 47 | return errors.New(fmt.Sprintf("YAML Parse Error: %v", err)) 48 | } 49 | 50 | return nil 51 | } 52 | 53 | // GeneralCheck GeneralCheck 54 | func (i inputs) GeneralCheck(kubeClientSet kubernetes.Interface) Results { 55 | 56 | var values podStruct 57 | 58 | // Set initial check results 59 | checkResult := Results{ 60 | DidPass: false, 61 | Message: "", 62 | } 63 | 64 | didValuesParse := false 65 | 66 | err := podParse(i.valuesYaml, &values) 67 | if err != nil { 68 | didValuesParse = true 69 | checkResult.Message = fmt.Sprintf("%v", err) 70 | } 71 | 72 | if !didValuesParse { 73 | 74 | // Get data from kubernetes 75 | pods, err := kubeClientSet.CoreV1().Pods(i.namespace).List(context.TODO(), metav1.ListOptions{}) 76 | if err != nil { 77 | panic(err.Error()) 78 | } 79 | 80 | // 81 | // Check pod state 82 | // 83 | didFindError := false 84 | for _, inputPod := range values.Values.ChecksEnabled.State { 85 | 86 | didFindContainer := false 87 | 88 | for _, aPod := range pods.Items { 89 | 90 | // Match string for the input pod 91 | match, _ := regexp.MatchString(inputPod.PodName, aPod.ObjectMeta.Name) 92 | 93 | if match { 94 | 95 | didFindContainer = true 96 | 97 | if inputPod.DesiredState == string(aPod.Status.Phase) { 98 | checkResult.DidPass = true 99 | checkResult.Message += "* Pod " + aPod.ObjectMeta.Name + " is in " + inputPod.DesiredState + " state\n" 100 | } else { 101 | checkResult.Message += "* Pod " + aPod.ObjectMeta.Name + " is NOT in " + inputPod.DesiredState + " state\n" 102 | didFindError = true 103 | } 104 | } 105 | } 106 | 107 | if !didFindContainer { 108 | checkResult.Message += "* Did not find pod: " + inputPod.PodName + "\n" 109 | didFindError = true 110 | } 111 | } 112 | 113 | // Found one or more errors 114 | if didFindError { 115 | checkResult.DidPass = false 116 | } 117 | 118 | } 119 | 120 | return checkResult 121 | } 122 | -------------------------------------------------------------------------------- /src/checks/kubernetes/pods/pods_test.go: -------------------------------------------------------------------------------- 1 | package pods 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | v1 "k8s.io/api/core/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/client-go/kubernetes" 10 | "k8s.io/client-go/kubernetes/fake" 11 | ) 12 | 13 | func TestNew(t *testing.T) { 14 | type args struct { 15 | valuesYaml string 16 | checkName string 17 | namespace string 18 | } 19 | tests := []struct { 20 | name string 21 | args args 22 | want inputs 23 | }{ 24 | // TODO: Add test cases. 25 | } 26 | for _, tt := range tests { 27 | t.Run(tt.name, func(t *testing.T) { 28 | if got := New(tt.args.valuesYaml, tt.args.checkName, tt.args.namespace); !reflect.DeepEqual(got, tt.want) { 29 | t.Errorf("New() = %v, want %v", got, tt.want) 30 | } 31 | }) 32 | } 33 | } 34 | 35 | func Test_podParse(t *testing.T) { 36 | type args struct { 37 | valuesYaml string 38 | v *podStruct 39 | } 40 | tests := []struct { 41 | name string 42 | args args 43 | wantErr bool 44 | }{ 45 | // TODO: Add test cases. 46 | } 47 | for _, tt := range tests { 48 | t.Run(tt.name, func(t *testing.T) { 49 | if err := podParse(tt.args.valuesYaml, tt.args.v); (err != nil) != tt.wantErr { 50 | t.Errorf("podParse() error = %v, wantErr %v", err, tt.wantErr) 51 | } 52 | }) 53 | } 54 | } 55 | 56 | func Test_inputs_GeneralCheck(t *testing.T) { 57 | type fields struct { 58 | valuesYaml string 59 | checkName string 60 | namespace string 61 | } 62 | type args struct { 63 | kubeClientSet kubernetes.Interface 64 | } 65 | tests := []struct { 66 | name string 67 | fields fields 68 | args args 69 | want Results 70 | }{ 71 | // TODO: Add test cases. 72 | { 73 | name: "Checking pod state - 2 container (positive)", 74 | fields: fields{ 75 | checkName: "check1", 76 | namespace: "ns1", 77 | valuesYaml: `--- 78 | values: 79 | checksEnabled: 80 | state: 81 | - podName: pod-1- 82 | desiredState: Running`, 83 | }, 84 | args: args{ 85 | // Doc/example: https://gianarb.it/blog/unit-testing-kubernetes-client-in-go 86 | kubeClientSet: fake.NewSimpleClientset(&v1.PodList{ 87 | Items: []v1.Pod{ 88 | { 89 | ObjectMeta: metav1.ObjectMeta{ 90 | Name: "pod-1-123abc-abc", 91 | Namespace: "ns1", 92 | Annotations: map[string]string{}, 93 | }, 94 | Status: v1.PodStatus{ 95 | Phase: "Running", 96 | }, 97 | }, 98 | { 99 | ObjectMeta: metav1.ObjectMeta{ 100 | Name: "pod-2222222-123abc-abc", 101 | Namespace: "ns1", 102 | Annotations: map[string]string{}, 103 | }, 104 | Status: v1.PodStatus{ 105 | Phase: "Running", 106 | }, 107 | }, 108 | }, 109 | }), 110 | }, 111 | want: Results{ 112 | DidPass: true, 113 | Message: "* Pod pod-1-123abc-abc is in Running state\n", 114 | }, 115 | }, 116 | { 117 | name: "Checking pod state - 1 container (positive)", 118 | fields: fields{ 119 | checkName: "check1", 120 | namespace: "ns1", 121 | valuesYaml: `--- 122 | values: 123 | checksEnabled: 124 | state: 125 | - podName: pod-1- 126 | desiredState: Running`, 127 | }, 128 | args: args{ 129 | // Doc/example: https://gianarb.it/blog/unit-testing-kubernetes-client-in-go 130 | kubeClientSet: fake.NewSimpleClientset(&v1.PodList{ 131 | Items: []v1.Pod{ 132 | { 133 | ObjectMeta: metav1.ObjectMeta{ 134 | Name: "pod-1-123abc-abc", 135 | Namespace: "ns1", 136 | Annotations: map[string]string{}, 137 | }, 138 | Status: v1.PodStatus{ 139 | Phase: "Running", 140 | }, 141 | }, 142 | }, 143 | }), 144 | }, 145 | want: Results{ 146 | DidPass: true, 147 | Message: "* Pod pod-1-123abc-abc is in Running state\n", 148 | }, 149 | }, 150 | { 151 | name: "Checking pod state - no containers found (negative)", 152 | fields: fields{ 153 | checkName: "check1", 154 | namespace: "ns1", 155 | valuesYaml: `--- 156 | values: 157 | checksEnabled: 158 | state: 159 | - podName: pod-1- 160 | desiredState: Running`, 161 | }, 162 | args: args{ 163 | // Doc/example: https://gianarb.it/blog/unit-testing-kubernetes-client-in-go 164 | kubeClientSet: fake.NewSimpleClientset(&v1.PodList{ 165 | Items: []v1.Pod{}, 166 | }), 167 | }, 168 | want: Results{ 169 | DidPass: false, 170 | Message: "* Did not find pod: pod-1-\n", 171 | }, 172 | }, 173 | { 174 | name: "Checking pod state - found one container but not another (negative)", 175 | fields: fields{ 176 | checkName: "check1", 177 | namespace: "ns1", 178 | valuesYaml: `--- 179 | values: 180 | checksEnabled: 181 | state: 182 | - podName: pod-1- 183 | desiredState: Running 184 | - podName: pod-2- 185 | desiredState: Running`, 186 | }, 187 | args: args{ 188 | // Doc/example: https://gianarb.it/blog/unit-testing-kubernetes-client-in-go 189 | kubeClientSet: fake.NewSimpleClientset(&v1.PodList{ 190 | Items: []v1.Pod{ 191 | { 192 | ObjectMeta: metav1.ObjectMeta{ 193 | Name: "pod-1-123abc-abc", 194 | Namespace: "ns1", 195 | Annotations: map[string]string{}, 196 | }, 197 | Status: v1.PodStatus{ 198 | Phase: "Running", 199 | }, 200 | }, 201 | }, 202 | }), 203 | }, 204 | want: Results{ 205 | DidPass: false, 206 | Message: `* Pod pod-1-123abc-abc is in Running state 207 | * Did not find pod: pod-2- 208 | `, 209 | }, 210 | }, 211 | { 212 | name: "Checking pod state - pod-2 in an incorrect state (negative)", 213 | fields: fields{ 214 | checkName: "check1", 215 | namespace: "ns1", 216 | valuesYaml: `--- 217 | values: 218 | checksEnabled: 219 | state: 220 | - podName: pod-1- 221 | desiredState: Running 222 | - podName: pod-2- 223 | desiredState: Running`, 224 | }, 225 | args: args{ 226 | // Doc/example: https://gianarb.it/blog/unit-testing-kubernetes-client-in-go 227 | kubeClientSet: fake.NewSimpleClientset(&v1.PodList{ 228 | Items: []v1.Pod{ 229 | { 230 | ObjectMeta: metav1.ObjectMeta{ 231 | Name: "pod-1-123abc-abc", 232 | Namespace: "ns1", 233 | Annotations: map[string]string{}, 234 | }, 235 | Status: v1.PodStatus{ 236 | Phase: "Running", 237 | }, 238 | }, 239 | { 240 | ObjectMeta: metav1.ObjectMeta{ 241 | Name: "pod-2-123abc-abc", 242 | Namespace: "ns1", 243 | Annotations: map[string]string{}, 244 | }, 245 | Status: v1.PodStatus{ 246 | Phase: "ContainerCreating", 247 | }, 248 | }, 249 | }, 250 | }), 251 | }, 252 | want: Results{ 253 | DidPass: false, 254 | Message: `* Pod pod-1-123abc-abc is in Running state 255 | * Pod pod-2-123abc-abc is NOT in Running state 256 | `, 257 | }, 258 | }, 259 | { 260 | name: "Checking pod state - pod-2 in an incorrect state - reverse pod order (negative)", 261 | fields: fields{ 262 | checkName: "check1", 263 | namespace: "ns1", 264 | valuesYaml: `--- 265 | values: 266 | checksEnabled: 267 | state: 268 | - podName: pod-2- 269 | desiredState: Running 270 | - podName: pod-1- 271 | desiredState: Running`, 272 | }, 273 | args: args{ 274 | // Doc/example: https://gianarb.it/blog/unit-testing-kubernetes-client-in-go 275 | kubeClientSet: fake.NewSimpleClientset(&v1.PodList{ 276 | Items: []v1.Pod{ 277 | { 278 | ObjectMeta: metav1.ObjectMeta{ 279 | Name: "pod-1-123abc-abc", 280 | Namespace: "ns1", 281 | Annotations: map[string]string{}, 282 | }, 283 | Status: v1.PodStatus{ 284 | Phase: "Running", 285 | }, 286 | }, 287 | { 288 | ObjectMeta: metav1.ObjectMeta{ 289 | Name: "pod-2-123abc-abc", 290 | Namespace: "ns1", 291 | Annotations: map[string]string{}, 292 | }, 293 | Status: v1.PodStatus{ 294 | Phase: "ContainerCreating", 295 | }, 296 | }, 297 | }, 298 | }), 299 | }, 300 | want: Results{ 301 | DidPass: false, 302 | Message: `* Pod pod-2-123abc-abc is NOT in Running state 303 | * Pod pod-1-123abc-abc is in Running state 304 | `, 305 | }, 306 | }, 307 | } 308 | for _, tt := range tests { 309 | t.Run(tt.name, func(t *testing.T) { 310 | i := inputs{ 311 | valuesYaml: tt.fields.valuesYaml, 312 | checkName: tt.fields.checkName, 313 | namespace: tt.fields.namespace, 314 | } 315 | if got := i.GeneralCheck(tt.args.kubeClientSet); !reflect.DeepEqual(got, tt.want) { 316 | t.Errorf("inputs.GeneralCheck() = %v, want %v", got, tt.want) 317 | } 318 | }) 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /src/checks/kubernetes/services/service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "gopkg.in/yaml.v2" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/client-go/kubernetes" 11 | ) 12 | 13 | // var kubeClientSet *kubernetes.Clientset 14 | 15 | type inputs struct { 16 | valuesYaml string 17 | checkName string 18 | namespace string 19 | } 20 | 21 | type Results struct { 22 | DidPass bool 23 | Message string 24 | } 25 | 26 | // New New 27 | func New(valuesYaml string, checkName string, namespace string) inputs { 28 | s := inputs{valuesYaml, checkName, namespace} 29 | 30 | return s 31 | } 32 | 33 | type serviceStruct struct { 34 | Values struct { 35 | ServiceName string `yaml:"serviceName"` 36 | Port int32 `yaml:"port"` 37 | ChecksEnabled struct { 38 | Ports bool `yaml:"ports"` 39 | Endpoints bool `yaml:"endpoints"` 40 | ClusterIP bool `yaml:"clusterIP"` 41 | HostPort bool `yaml:"hostPort"` 42 | } `yaml:"checksEnabled"` 43 | } `yaml:"values"` 44 | } 45 | 46 | func serviceParse(valuesYaml string, v *serviceStruct) error { 47 | 48 | err := yaml.Unmarshal([]byte(valuesYaml), &v) 49 | if err != nil { 50 | return errors.New(fmt.Sprintf("YAML Parse Error: %v", err)) 51 | } 52 | 53 | if v.Values.ServiceName == "" { 54 | return errors.New("Check values: no `ServiceName` set") 55 | } 56 | 57 | if v.Values.Port < 1 || v.Values.Port > 65353 { 58 | return errors.New("Check values: invalid `Port` specified, allowed range (1 - 65353)") 59 | } 60 | 61 | return nil 62 | } 63 | 64 | // GeneralCheck GeneralCheck 65 | func (i inputs) GeneralCheck(kubeClientSet kubernetes.Interface) Results { 66 | 67 | var values serviceStruct 68 | 69 | // Set initial check results 70 | checkResult := Results{ 71 | DidPass: false, 72 | Message: "", 73 | } 74 | 75 | didValuesParse := false 76 | 77 | err := serviceParse(i.valuesYaml, &values) 78 | if err != nil { 79 | didValuesParse = true 80 | checkResult.Message = fmt.Sprintf("%v", err) 81 | } 82 | 83 | if !didValuesParse { 84 | // Run kube stuff 85 | services, err := kubeClientSet.CoreV1().Services(i.namespace).List(context.TODO(), metav1.ListOptions{}) 86 | if err != nil { 87 | panic(err.Error()) 88 | } 89 | 90 | // Loop through all of the services found 91 | for _, aService := range services.Items { 92 | 93 | // Find the service with the name we are interested in 94 | if aService.ObjectMeta.Name == values.Values.ServiceName { 95 | // 96 | // Run only enabled checks 97 | // 98 | 99 | if values.Values.ChecksEnabled.ClusterIP { 100 | if aService.Spec.ClusterIP == "" { 101 | checkResult.DidPass = false 102 | checkResult.Message += "* No ClusterIP Found\n" 103 | } else { 104 | checkResult.DidPass = true 105 | checkResult.Message += "* ClusterIP Found\n" 106 | } 107 | } 108 | 109 | if values.Values.ChecksEnabled.Endpoints { 110 | // Look to see if the endpoints for this service exists or not 111 | endpoints, err := kubeClientSet.CoreV1().Endpoints(i.namespace).List(context.TODO(), metav1.ListOptions{}) 112 | if err != nil { 113 | panic(err.Error()) 114 | } 115 | 116 | for _, anEndpoint := range endpoints.Items { 117 | if anEndpoint.ObjectMeta.Name == values.Values.ServiceName { 118 | if len(anEndpoint.Subsets) == 1 { 119 | if len(anEndpoint.Subsets[0].Addresses) > 0 { 120 | for _, anAddress := range anEndpoint.Subsets[0].Addresses { 121 | if anAddress.IP != "" { 122 | checkResult.DidPass = true 123 | checkResult.Message += "* Endpoint found: " + anAddress.IP + "\n" 124 | } else { 125 | checkResult.DidPass = false 126 | checkResult.Message += "* No Endpoint found in the Subsets[0].Addresses[x].IP field\n" 127 | } 128 | } 129 | } else { 130 | checkResult.DidPass = false 131 | checkResult.Message += "* No Endpoint found in the Subsets[0].Addresses list\n" 132 | } 133 | } else { 134 | checkResult.DidPass = false 135 | checkResult.Message += "* No Endpoint found in the subsets list\n" 136 | } 137 | } 138 | } 139 | } 140 | 141 | if values.Values.ChecksEnabled.HostPort { 142 | // TBD 143 | } 144 | 145 | if values.Values.ChecksEnabled.Ports { 146 | for _, port := range aService.Spec.Ports { 147 | if port.Port != values.Values.Port { 148 | checkResult.DidPass = false 149 | checkResult.Message += "* Port NOT found: " + fmt.Sprint(values.Values.Port) + "\n" 150 | } else { 151 | checkResult.DidPass = true 152 | checkResult.Message += "* Port found: " + fmt.Sprint(values.Values.Port) + "\n" 153 | } 154 | } 155 | } 156 | } 157 | } 158 | 159 | } 160 | 161 | return checkResult 162 | } 163 | -------------------------------------------------------------------------------- /src/checks/kubernetes/services/service_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | v1 "k8s.io/api/core/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/client-go/kubernetes" 10 | "k8s.io/client-go/kubernetes/fake" 11 | ) 12 | 13 | func Test_serviceParse(t *testing.T) { 14 | type args struct { 15 | valuesYaml string 16 | v *serviceStruct 17 | } 18 | tests := []struct { 19 | name string 20 | args args 21 | wantErr bool 22 | }{ 23 | { 24 | name: "test1 - success", 25 | args: args{ 26 | valuesYaml: "values:\n serviceName: service1\n port: 20014", 27 | v: &serviceStruct{}, 28 | }, 29 | wantErr: false, 30 | }, 31 | { 32 | name: "test2 - no serviceName param present", 33 | args: args{ 34 | valuesYaml: "values:\n port: 20014", 35 | v: &serviceStruct{}, 36 | }, 37 | wantErr: true, 38 | }, 39 | { 40 | name: "test3 - no port param present", 41 | args: args{ 42 | valuesYaml: "values:\n serviceName: service1", 43 | v: &serviceStruct{}, 44 | }, 45 | wantErr: true, 46 | }, 47 | { 48 | name: "test4 - invalid port range high", 49 | args: args{ 50 | valuesYaml: "values:\n serviceName: service1\n port: 65354", 51 | v: &serviceStruct{}, 52 | }, 53 | wantErr: true, 54 | }, 55 | { 56 | name: "test4 - invalid port range high", 57 | args: args{ 58 | valuesYaml: "values:\n serviceName: service1\n port: -1", 59 | v: &serviceStruct{}, 60 | }, 61 | wantErr: true, 62 | }, 63 | } 64 | for _, tt := range tests { 65 | t.Run(tt.name, func(t *testing.T) { 66 | if err := serviceParse(tt.args.valuesYaml, tt.args.v); (err != nil) != tt.wantErr { 67 | t.Errorf("serviceParse() name = %v, error = %v, wantErr %v", tt.name, err, tt.wantErr) 68 | } 69 | }) 70 | } 71 | } 72 | 73 | func TestNew(t *testing.T) { 74 | type args struct { 75 | valuesYaml string 76 | checkName string 77 | namespace string 78 | } 79 | tests := []struct { 80 | name string 81 | args args 82 | want inputs 83 | }{ 84 | // TODO: Add test cases. 85 | } 86 | for _, tt := range tests { 87 | t.Run(tt.name, func(t *testing.T) { 88 | if got := New(tt.args.valuesYaml, tt.args.checkName, tt.args.namespace); !reflect.DeepEqual(got, tt.want) { 89 | t.Errorf("New() = %v, want %v", got, tt.want) 90 | } 91 | }) 92 | } 93 | } 94 | 95 | func Test_inputs_GeneralCheck(t *testing.T) { 96 | type fields struct { 97 | valuesYaml string 98 | checkName string 99 | namespace string 100 | } 101 | type args struct { 102 | kubeClientSet kubernetes.Interface 103 | } 104 | tests := []struct { 105 | name string 106 | fields fields 107 | args args 108 | want Results 109 | }{ 110 | // TODO: Add test cases. 111 | { 112 | name: "Checking clusterIP (positive)", 113 | fields: fields{ 114 | checkName: "check1", 115 | namespace: "ns1", 116 | valuesYaml: "values:\n serviceName: service1\n port: 20014\n checksEnabled:\n clusterIP: true", 117 | }, 118 | args: args{ 119 | // Doc/example: https://gianarb.it/blog/unit-testing-kubernetes-client-in-go 120 | kubeClientSet: fake.NewSimpleClientset(&v1.ServiceList{ 121 | Items: []v1.Service{ 122 | { 123 | ObjectMeta: metav1.ObjectMeta{ 124 | Name: "service1", 125 | Namespace: "ns1", 126 | Annotations: map[string]string{}, 127 | }, 128 | Spec: v1.ServiceSpec{ 129 | ClusterIP: "1.1.1.1", 130 | }, 131 | }, 132 | }, 133 | }), 134 | }, 135 | want: Results{ 136 | DidPass: true, 137 | Message: "* ClusterIP Found\n", 138 | }, 139 | }, 140 | { 141 | name: "Checking clusterIP (negative)", 142 | fields: fields{ 143 | checkName: "check1", 144 | namespace: "ns1", 145 | valuesYaml: "values:\n serviceName: service1\n port: 20014\n checksEnabled:\n clusterIP: true", 146 | }, 147 | args: args{ 148 | // Doc/example: https://gianarb.it/blog/unit-testing-kubernetes-client-in-go 149 | kubeClientSet: fake.NewSimpleClientset(&v1.ServiceList{ 150 | Items: []v1.Service{ 151 | { 152 | ObjectMeta: metav1.ObjectMeta{ 153 | Name: "service1", 154 | Namespace: "ns1", 155 | Annotations: map[string]string{}, 156 | }, 157 | Spec: v1.ServiceSpec{ 158 | // No ClusterIP and this will fail the test 159 | // ClusterIP: "1.1.1.1", 160 | }, 161 | }, 162 | }, 163 | }), 164 | }, 165 | want: Results{ 166 | DidPass: false, 167 | Message: "* No ClusterIP Found\n", 168 | }, 169 | }, 170 | // TODO: Fix this test: The endpoint k8s fake info doesnt show up. The endpoint data is in the second k8s call. Not sure why 171 | // { 172 | // name: "Checking Endpoints (positive)", 173 | // fields: fields{ 174 | // checkName: "check2", 175 | // namespace: "ns1", 176 | // valuesYaml: "values:\n serviceName: service1\n port: 20014\n checksEnabled:\n endpoints: true", 177 | // }, 178 | // args: args{ 179 | // // Doc/example: https://gianarb.it/blog/unit-testing-kubernetes-client-in-go 180 | // kubeClientSet: fake.NewSimpleClientset(&v1.EndpointsList{ 181 | // Items: []v1.Endpoints{ 182 | // { 183 | // ObjectMeta: metav1.ObjectMeta{ 184 | // Name: "service1", 185 | // }, 186 | // Subsets: []v1.EndpointSubset{ 187 | // { 188 | // Addresses: []v1.EndpointAddress{ 189 | // { 190 | // IP: "1.1.1.1", 191 | // }, 192 | // }, 193 | // }, 194 | // }, 195 | // }, 196 | // }, 197 | // }, &v1.ServiceList{ 198 | // Items: []v1.Service{ 199 | // { 200 | // ObjectMeta: metav1.ObjectMeta{ 201 | // Name: "service1", 202 | // Namespace: "ns1", 203 | // Annotations: map[string]string{}, 204 | // }, 205 | // Spec: v1.ServiceSpec{ 206 | // ClusterIP: "1.1.1.1", 207 | // }, 208 | // }, 209 | // }, 210 | // }), 211 | // }, 212 | // want: Results{ 213 | // DidPass: true, 214 | // Message: "* ClusterIP Found\n", 215 | // }, 216 | // }, 217 | { 218 | name: "Checking ports (positive)", 219 | fields: fields{ 220 | checkName: "check3", 221 | namespace: "ns1", 222 | valuesYaml: "values:\n serviceName: service1\n port: 20014\n checksEnabled:\n ports: true", 223 | }, 224 | args: args{ 225 | // Doc/example: https://gianarb.it/blog/unit-testing-kubernetes-client-in-go 226 | kubeClientSet: fake.NewSimpleClientset(&v1.ServiceList{ 227 | Items: []v1.Service{ 228 | { 229 | ObjectMeta: metav1.ObjectMeta{ 230 | Name: "service1", 231 | Namespace: "ns1", 232 | Annotations: map[string]string{}, 233 | }, 234 | Spec: v1.ServiceSpec{ 235 | ClusterIP: "1.1.1.1", 236 | Ports: []v1.ServicePort{ 237 | { 238 | Port: 20014, 239 | }, 240 | }, 241 | }, 242 | }, 243 | }, 244 | }), 245 | }, 246 | want: Results{ 247 | DidPass: true, 248 | Message: "* Port found: 20014\n", 249 | }, 250 | }, 251 | { 252 | name: "Checking ports (negative)", 253 | fields: fields{ 254 | checkName: "check4", 255 | namespace: "ns1", 256 | valuesYaml: "values:\n serviceName: service1\n port: 20014\n checksEnabled:\n ports: true", 257 | }, 258 | args: args{ 259 | // Doc/example: https://gianarb.it/blog/unit-testing-kubernetes-client-in-go 260 | kubeClientSet: fake.NewSimpleClientset(&v1.ServiceList{ 261 | Items: []v1.Service{ 262 | { 263 | ObjectMeta: metav1.ObjectMeta{ 264 | Name: "service1", 265 | Namespace: "ns1", 266 | Annotations: map[string]string{}, 267 | }, 268 | Spec: v1.ServiceSpec{ 269 | ClusterIP: "1.1.1.1", 270 | Ports: []v1.ServicePort{ 271 | { 272 | // This is the incorrect port that will fail the test 273 | Port: 20013, 274 | }, 275 | }, 276 | }, 277 | }, 278 | }, 279 | }), 280 | }, 281 | want: Results{ 282 | DidPass: false, 283 | Message: "* Port NOT found: 20014\n", 284 | }, 285 | }, 286 | { 287 | name: "Checking ports (negative) - port doesnt exist", 288 | fields: fields{ 289 | checkName: "check3", 290 | namespace: "ns1", 291 | valuesYaml: "values:\n serviceName: service1\n port: 20014\n checksEnabled:\n clusterIP: true\n endpoints: true\n hostPort: false\n ports: true", 292 | }, 293 | args: args{ 294 | // Doc/example: https://gianarb.it/blog/unit-testing-kubernetes-client-in-go 295 | kubeClientSet: fake.NewSimpleClientset(&v1.ServiceList{ 296 | Items: []v1.Service{ 297 | { 298 | ObjectMeta: metav1.ObjectMeta{ 299 | Name: "service1", 300 | Namespace: "ns1", 301 | Annotations: map[string]string{}, 302 | }, 303 | Spec: v1.ServiceSpec{ 304 | ClusterIP: "1.1.1.1", 305 | Ports: []v1.ServicePort{ 306 | { 307 | Port: 20015, 308 | }, 309 | }, 310 | }, 311 | }, 312 | }, 313 | }, &v1.EndpointsList{ 314 | Items: []v1.Endpoints{ 315 | { 316 | ObjectMeta: metav1.ObjectMeta{ 317 | Name: "service1", 318 | Namespace: "ns1", 319 | Annotations: map[string]string{}, 320 | }, 321 | Subsets: []v1.EndpointSubset{ 322 | { 323 | Addresses: []v1.EndpointAddress{ 324 | { 325 | IP: "2.2.2.2", 326 | }, 327 | }, 328 | }, 329 | }, 330 | }, 331 | }, 332 | }), 333 | }, 334 | want: Results{ 335 | DidPass: false, 336 | Message: "* ClusterIP Found\n* Endpoint found: 2.2.2.2\n* Port NOT found: 20014\n", 337 | }, 338 | }, 339 | } 340 | for _, tt := range tests { 341 | t.Run(tt.name, func(t *testing.T) { 342 | i := inputs{ 343 | valuesYaml: tt.fields.valuesYaml, 344 | checkName: tt.fields.checkName, 345 | namespace: tt.fields.namespace, 346 | } 347 | if got := i.GeneralCheck(tt.args.kubeClientSet); !reflect.DeepEqual(got, tt.want) { 348 | t.Errorf("%v: inputs.GeneralCheck() = %v, want %v", tt.name, got, tt.want) 349 | } 350 | }) 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /src/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "reflect" 11 | "strconv" 12 | "unsafe" 13 | 14 | "github.com/anthem-ai/kubernetes-state-checker/src/checker" 15 | 16 | "github.com/olekukonko/tablewriter" 17 | yaml "gopkg.in/yaml.v2" 18 | 19 | "k8s.io/client-go/kubernetes" 20 | "k8s.io/client-go/tools/clientcmd" 21 | "k8s.io/client-go/util/homedir" 22 | ) 23 | 24 | var kubeNamespace string 25 | var kscConfig string 26 | 27 | type conf struct { 28 | Some string `yaml:"some"` 29 | Random int64 `yaml:"random"` 30 | KubernetesStateChecker []checker.Check `yaml:"kubernetes-state-checker"` 31 | } 32 | 33 | func (c *conf) getConf() *conf { 34 | 35 | configFileLocation := "" 36 | 37 | // Input config from CLI flags 38 | if kscConfig != "" { 39 | configFileLocation = kscConfig 40 | } else { 41 | // Input config file from environment variable 42 | configFileLocation = os.Getenv("KSC_CONFIG") 43 | 44 | if configFileLocation == "" { 45 | fmt.Println("ERROR: You must set the environment variable KSC_CONFIG which points to the input config file.") 46 | os.Exit(1) 47 | } 48 | } 49 | 50 | yamlFile, err := ioutil.ReadFile(configFileLocation) 51 | if err != nil { 52 | log.Printf("yamlFile.Get err #%v ", err) 53 | } 54 | err = yaml.Unmarshal(yamlFile, c) 55 | if err != nil { 56 | log.Fatalf("Unmarshal: %v", err) 57 | } 58 | 59 | return c 60 | } 61 | 62 | func init() { 63 | // kubernetes namespace override 64 | flag.StringVar(&kubeNamespace, "namespace", "", "(optional) to override the namespace value in the input config file") 65 | flag.StringVar(&kubeNamespace, "n", "", "(optional) to override the namespace value in the input config file") 66 | 67 | // kubernetes-state-checker input config via cli flag 68 | flag.StringVar(&kscConfig, "config", "", "(optional) a flag to set the config file. Will override the environment variable KSC_CONFIG") 69 | flag.StringVar(&kscConfig, "c", "", "(optional) a flag to set the config file. Will override the environment variable KSC_CONFIG") 70 | } 71 | 72 | func main() { 73 | 74 | // Get kubeconfig 75 | // kubeconfig setup example: https://github.com/kubernetes/client-go/blob/master/examples/out-of-cluster-client-configuration/main.go 76 | var kubeconfig *string 77 | if home := homedir.HomeDir(); home != "" { 78 | kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file") 79 | } else { 80 | kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file") 81 | } 82 | 83 | // Parse all flags 84 | flag.Parse() 85 | 86 | // use the current context in kubeconfig 87 | config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig) 88 | if err != nil { 89 | panic(err.Error()) 90 | } 91 | 92 | // create the clientset 93 | clientset, err := kubernetes.NewForConfig(config) 94 | if err != nil { 95 | panic(err.Error()) 96 | } 97 | 98 | // Get input yaml with checks 99 | var c conf 100 | c.getConf() 101 | 102 | // Output data 103 | outpuData := [][]string{} 104 | 105 | tmpCounter := 0 106 | 107 | for _, aCheck := range c.KubernetesStateChecker { 108 | 109 | // convert to yaml 110 | valuesYaml, err := yaml.Marshal(aCheck) 111 | if err != nil { 112 | panic(err.Error()) 113 | } 114 | 115 | // Use namespace override or not 116 | namespace := aCheck.Namespace 117 | if kubeNamespace != "" { 118 | namespace = kubeNamespace 119 | fmt.Println("Setting from input flag") 120 | } 121 | 122 | // Execute the check runner 123 | chk := checker.New( 124 | BytesToString(valuesYaml), 125 | clientset, 126 | aCheck.Ttype, 127 | aCheck.Name, 128 | aCheck.Description, 129 | namespace, 130 | aCheck.Values, 131 | ) 132 | results := chk.Run() 133 | 134 | outpuData = append(outpuData, []string{aCheck.Ttype, c.KubernetesStateChecker[tmpCounter].Name, strconv.FormatBool(results.DidPass), results.Message}) 135 | 136 | tmpCounter++ 137 | } 138 | 139 | table := tablewriter.NewWriter(os.Stdout) 140 | table.SetHeader([]string{"Test Type", "Name", "Status", "Message"}) 141 | table.SetRowLine(true) 142 | table.SetReflowDuringAutoWrap(false) 143 | table.SetColWidth(100) 144 | 145 | for _, v := range outpuData { 146 | table.Append(v) 147 | } 148 | table.Render() // Send output 149 | 150 | } 151 | 152 | func BytesToString(b []byte) string { 153 | bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) 154 | sh := reflect.StringHeader{bh.Data, bh.Len} 155 | return *(*string)(unsafe.Pointer(&sh)) 156 | } 157 | --------------------------------------------------------------------------------