├── ui-app ├── .env ├── src │ ├── version.js │ ├── react-app-env.d.ts │ ├── setupTests.ts │ ├── App.test.tsx │ ├── PodList.css │ ├── index.css │ ├── reportWebVitals.ts │ ├── App.css │ ├── dto.ts │ ├── utils.ts │ ├── Pod.tsx │ ├── index.tsx │ ├── FileViewerWrapper.tsx │ ├── ProcessExplorer.tsx │ ├── logo.svg │ ├── FileViewer.tsx │ ├── App.tsx │ ├── PodList.tsx │ └── FileExplorer.tsx ├── public │ ├── robots.txt │ ├── favicon.png │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── .gitignore ├── tsconfig.json ├── package.json └── README.md ├── 0.png ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── doc.go ├── .gitignore ├── procs.go ├── docker └── Dockerfile ├── go.mod ├── files.go ├── pods.go ├── main.go ├── README.md ├── k8s.go └── go.sum /ui-app/.env: -------------------------------------------------------------------------------- 1 | BUILD_PATH='./../www' 2 | -------------------------------------------------------------------------------- /ui-app/src/version.js: -------------------------------------------------------------------------------- 1 | export const VERSION = 'v2021.10.04'; 2 | -------------------------------------------------------------------------------- /0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangjia184/pod-inspector/HEAD/0.png -------------------------------------------------------------------------------- /1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangjia184/pod-inspector/HEAD/1.png -------------------------------------------------------------------------------- /2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangjia184/pod-inspector/HEAD/2.png -------------------------------------------------------------------------------- /3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangjia184/pod-inspector/HEAD/3.png -------------------------------------------------------------------------------- /4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangjia184/pod-inspector/HEAD/4.png -------------------------------------------------------------------------------- /ui-app/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // PodInspector project doc.go 2 | 3 | /* 4 | PodInspector document 5 | */ 6 | package main 7 | -------------------------------------------------------------------------------- /ui-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /ui-app/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangjia184/pod-inspector/HEAD/ui-app/public/favicon.png -------------------------------------------------------------------------------- /ui-app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangjia184/pod-inspector/HEAD/ui-app/public/logo192.png -------------------------------------------------------------------------------- /ui-app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangjia184/pod-inspector/HEAD/ui-app/public/logo512.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /pod-inspector.exe 2 | /pod-inspector.debug.exe 3 | /pod-inspector 4 | /www-app/package-lock.json 5 | /docker/bin 6 | /docker/www 7 | /www 8 | /PodList.json 9 | /PodListMetrics.json 10 | /spec -------------------------------------------------------------------------------- /procs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func GetPsResult(podName string, containerName string, namespace string, token string) ([]byte, error) { 4 | 5 | cmd := []string{"ps", "auxf"} 6 | return execCmd(podName, containerName, namespace, token, cmd) 7 | } 8 | -------------------------------------------------------------------------------- /ui-app/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /ui-app/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /ui-app/src/PodList.css: -------------------------------------------------------------------------------- 1 | .DetailsListColumnRight .ms-DetailsHeader-cellTitle { flex-direction : row-reverse !important; } 2 | 3 | .container{ 4 | display: flex; 5 | align-items: center; 6 | } 7 | 8 | .PodList .ms-DetailsRow-fields { align-items: center !important;} 9 | .PodList .ms-DetailsRow-cell { font-size: 14px; font-family: monospace, 'Courier New', Courier;} -------------------------------------------------------------------------------- /ui-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /www 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /ui-app/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | overflow-x: hidden; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /ui-app/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /ui-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /ui-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /ui-app/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3 2 | 3 | 4 | # https://medium.com/@artur.klauser/building-multi-architecture-docker-images-with-buildx-27d80f7e2408 5 | 6 | LABEL maintainer="Jerry.Wang" 7 | 8 | RUN apk --update add bash ca-certificates; \ 9 | adduser -D -H -s /bin/false app_user app_user; \ 10 | apk del bash; \ 11 | mkdir /app; 12 | 13 | 14 | # The TARGETPLATFORM ARG variable is set automatically: https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope 15 | ARG TARGETPLATFORM 16 | COPY bin/${TARGETPLATFORM}/pod-inspector /app/pod-inspector 17 | ADD www /app/www 18 | 19 | RUN chmod +x /app/pod-inspector; 20 | 21 | WORKDIR /app 22 | 23 | EXPOSE 8080/tcp 24 | 25 | #USER app_user 26 | ENTRYPOINT ["/app/pod-inspector"] 27 | 28 | -------------------------------------------------------------------------------- /ui-app/src/dto.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IContainer { 3 | ready: boolean; 4 | restartCount: number; 5 | cpuUsage: number; 6 | cpuLimit: number; 7 | ramUsage: number; 8 | ramLimit: number; 9 | } 10 | 11 | export interface IPodFile { 12 | name: string; 13 | path: string; 14 | isDir: boolean; 15 | size: number | undefined; 16 | time: string; 17 | timestamp: number; 18 | } 19 | 20 | export default interface IPod { 21 | name: string; 22 | status: string; 23 | age: number; 24 | ready: number; 25 | hostIp: string; 26 | podIp: string; 27 | cpuUsage: number; 28 | cpuLimit: number; 29 | cpuPercentage: number; 30 | ramUsage: number; 31 | ramLimit: number; 32 | ramPercentage: number; 33 | containers : Map; 34 | } 35 | 36 | 37 | -------------------------------------------------------------------------------- /ui-app/src/utils.ts: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | 3 | export const getStorageValue = function(key : string, defaultValue : any) { 4 | // getting stored value 5 | const saved = localStorage.getItem(key); 6 | const initial = JSON.parse(saved!); 7 | return initial || defaultValue; 8 | } 9 | 10 | export const useLocalStorage = (key : string, defaultValue : any) => { 11 | const [value, setValue] = useState(() => { 12 | return getStorageValue(key, defaultValue); 13 | }); 14 | 15 | useEffect(() => { 16 | // storing input name 17 | localStorage.setItem(key, JSON.stringify(value)); 18 | }, [key, value]); 19 | 20 | return [value, setValue]; 21 | }; 22 | 23 | 24 | 25 | export const K8sToken = React.createContext('K8S_TOKEN'); 26 | export const K8sNamespace = React.createContext('K8S_NAMESPACE'); -------------------------------------------------------------------------------- /ui-app/src/Pod.tsx: -------------------------------------------------------------------------------- 1 | import React, { } from 'react'; 2 | import { Pivot, PivotItem } from '@fluentui/react'; 3 | import FileExplorer from './FileExplorer' 4 | import ProcessExplorer from './ProcessExplorer' 5 | import IPod from './dto' 6 | 7 | export interface IPodComponentProps { 8 | value : IPod, 9 | containerName : string, 10 | } 11 | 12 | const Pod: React.FunctionComponent = ({ value : pod, containerName }) => { 13 | 14 | 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default Pod; 30 | -------------------------------------------------------------------------------- /ui-app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | // Also available from @uifabric/icons (7 and earlier) and @fluentui/font-icons-mdl2 (8+) 4 | import { initializeIcons } from '@fluentui/react/lib/Icons'; 5 | import { initializeFileTypeIcons } from '@fluentui/react-file-type-icons'; 6 | import './index.css'; 7 | import App from './App'; 8 | import reportWebVitals from './reportWebVitals'; 9 | 10 | initializeIcons(/* optional base url */); 11 | initializeFileTypeIcons(/* optional base url */); 12 | 13 | ReactDOM.render( 14 | 15 | 16 | , 17 | document.getElementById('root') 18 | ); 19 | 20 | // If you want to start measuring performance in your app, pass a function 21 | // to log results (for example: reportWebVitals(console.log)) 22 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 23 | reportWebVitals(); 24 | -------------------------------------------------------------------------------- /ui-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fluentui/react": "~8.34.3", 7 | "@fluentui/react-file-type-icons": "~8.4.0", 8 | "@testing-library/jest-dom": "~5.11.4", 9 | "@testing-library/react": "~11.1.0", 10 | "@testing-library/user-event": "~12.1.10", 11 | "@types/jest": "~26.0.15", 12 | "@types/node": "~12.0.0", 13 | "@types/react": "~17.0.0", 14 | "@types/react-dom": "~17.0.0", 15 | "ace-builds": "~1.4.12", 16 | "react": "~17.0.2", 17 | "react-ace": "~9.4.4", 18 | "react-dom": "~17.0.2", 19 | "react-router-dom": "~5.3.0", 20 | "react-scripts": "4.0.3", 21 | "typescript": "~4.1.2", 22 | "web-vitals": "~1.0.1" 23 | }, 24 | "scripts": { 25 | "start": "set REACT_APP_BASE_URL=http://127.0.0.1:8080 && react-scripts start", 26 | "build": "react-scripts build", 27 | "test": "react-scripts test", 28 | "eject": "react-scripts eject" 29 | }, 30 | "eslintConfig": { 31 | "extends": [ 32 | "react-app", 33 | "react-app/jest" 34 | ] 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "@types/react-router-dom": "~5.1.9" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ui-app/src/FileViewerWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FontIcon } from '@fluentui/react/lib/Icon'; 3 | import { Text } from '@fluentui/react/lib/Text'; 4 | import { Stack } from '@fluentui/react'; 5 | import { Separator } from '@fluentui/react/lib/Separator'; 6 | import FileViewer from './FileViewer' 7 | 8 | 9 | 10 | 11 | const FileViewWrapper: React.FunctionComponent = (props : any) => { 12 | 13 | const k8sNamespace = props.match.params.namespace; 14 | const podName = props.match.params.pod; 15 | const containerName = props.match.params.container; 16 | const filepath = '/' + props.match.params.filepath; 17 | 18 | return ( 19 | <> 20 | 21 |
22 | 29 | 30 | {podName} 31 | 32 | 33 | 34 | 35 | 36 | {containerName} 37 | 38 | 39 | 40 | 41 | 42 | {filepath} 43 | 44 | 45 | 46 | 47 |
48 | 49 | ); 50 | }; 51 | 52 | 53 | export default FileViewWrapper; 54 | -------------------------------------------------------------------------------- /ui-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | K8S Pod Inspector 28 | 29 | 30 | 31 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /ui-app/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | * NodeJS v14 LTS 6 | * Yarn v1.22 7 | 8 | ## Available Scripts 9 | 10 | In the project directory, you can run: 11 | 12 | ### `yarn start` 13 | 14 | Runs the app in the development mode.\ 15 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 16 | 17 | The page will reload if you make edits.\ 18 | You will also see any lint errors in the console. 19 | 20 | ### `yarn test` 21 | 22 | Launches the test runner in the interactive watch mode.\ 23 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 24 | 25 | ### `yarn build` 26 | 27 | Builds the app for production to the `build` folder.\ 28 | It correctly bundles React in production mode and optimizes the build for the best performance. 29 | 30 | The build is minified and the filenames include the hashes.\ 31 | Your app is ready to be deployed! 32 | 33 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 34 | 35 | ### `yarn eject` 36 | 37 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 38 | 39 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 40 | 41 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 42 | 43 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 44 | 45 | ## Learn More 46 | 47 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 48 | 49 | To learn React, check out the [React documentation](https://reactjs.org/). 50 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wangjia184/pod-inspector 2 | 3 | go 1.17 4 | 5 | require github.com/gin-gonic/gin v1.7.4 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/gin-contrib/cors v1.3.1 // indirect 10 | github.com/gin-contrib/sse v0.1.0 // indirect 11 | github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19 // indirect 12 | github.com/go-logr/logr v0.4.0 // indirect 13 | github.com/go-playground/locales v0.14.0 // indirect 14 | github.com/go-playground/universal-translator v0.18.0 // indirect 15 | github.com/go-playground/validator/v10 v10.9.0 // indirect 16 | github.com/gogo/protobuf v1.3.2 // indirect 17 | github.com/golang/protobuf v1.5.2 // indirect 18 | github.com/google/go-cmp v0.5.5 // indirect 19 | github.com/google/gofuzz v1.1.0 // indirect 20 | github.com/googleapis/gnostic v0.5.5 // indirect 21 | github.com/imdario/mergo v0.3.5 // indirect 22 | github.com/json-iterator/go v1.1.12 // indirect 23 | github.com/leodido/go-urn v1.2.1 // indirect 24 | github.com/mattn/go-isatty v0.0.14 // indirect 25 | github.com/moby/spdystream v0.2.0 // indirect 26 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 27 | github.com/modern-go/reflect2 v1.0.2 // indirect 28 | github.com/spf13/pflag v1.0.5 // indirect 29 | github.com/ugorji/go/codec v1.2.6 // indirect 30 | golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect 31 | golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 // indirect 32 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect 33 | golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 // indirect 34 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect 35 | golang.org/x/text v0.3.7 // indirect 36 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect 37 | google.golang.org/appengine v1.6.5 // indirect 38 | google.golang.org/protobuf v1.27.1 // indirect 39 | gopkg.in/inf.v0 v0.9.1 // indirect 40 | gopkg.in/yaml.v2 v2.4.0 // indirect 41 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 42 | k8s.io/api v0.22.2 // indirect 43 | k8s.io/apimachinery v0.22.2 // indirect 44 | k8s.io/client-go v0.22.2 // indirect 45 | k8s.io/klog/v2 v2.9.0 // indirect 46 | k8s.io/metrics v0.22.2 // indirect 47 | k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a // indirect 48 | sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect 49 | sigs.k8s.io/yaml v1.2.0 // indirect 50 | ) 51 | -------------------------------------------------------------------------------- /ui-app/src/ProcessExplorer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useContext } from 'react'; 2 | import { Separator } from '@fluentui/react/lib/Separator'; 3 | import { mergeStyleSets } from '@fluentui/react/lib/Styling'; 4 | import { K8sToken, K8sNamespace } from './utils' 5 | import IPod from './dto' 6 | 7 | 8 | const classNames = mergeStyleSets({ 9 | psResult: { 10 | padding: 0, 11 | fontSize: '16px', 12 | 'pre': { 13 | padding : '5px', 14 | color: 'white', 15 | backgroundColor : 'black', 16 | overflowY: 'auto', 17 | overflowX: 'auto', 18 | width: '100%', 19 | height: 'calc(100vh - 200px)', 20 | fontFamily : 'monospace, "Lucida Console", Monaco, "Courier New", Courier, fixedsys' 21 | } 22 | }, 23 | 24 | }); 25 | 26 | 27 | 28 | 29 | 30 | 31 | export interface IProcessExplorerComponentProps { 32 | pod : IPod, 33 | containerName : string, 34 | } 35 | 36 | 37 | const ProcessExplorer: React.FunctionComponent = ({ pod, containerName }) => { 38 | 39 | const k8sToken = useContext(K8sToken); 40 | const k8sNamespace = useContext(K8sNamespace); 41 | const [psResult, setPsResult] = useState(""); 42 | 43 | useEffect( () => { 44 | let currentPodName = pod.name; 45 | let currencyContainerName = containerName; 46 | 47 | var url = (process.env.REACT_APP_BASE_URL || '').trim() + "/api/pod/" + currentPodName + '/' + currencyContainerName + '/process/list?'; 48 | url += new URLSearchParams({ 49 | token : k8sToken, 50 | namespace : k8sNamespace, 51 | }).toString(); 52 | 53 | 54 | const fetchData = async () => { 55 | try { 56 | const response = await fetch(url, {mode:'cors'}); 57 | const json = await response.json(); 58 | if(currentPodName !== pod.name || currencyContainerName !== containerName){ 59 | return; // response is not for the current UI 60 | } 61 | setPsResult(json.result); 62 | 63 | } catch (error) { 64 | setPsResult("Error : " + error); 65 | } 66 | }; 67 | fetchData(); 68 | }, [pod, containerName, k8sToken, k8sNamespace]); 69 | 70 | 71 | return ( 72 | <> 73 | 74 | 75 |
76 |
{psResult}
77 |
78 | 79 | ); 80 | }; 81 | 82 | 83 | export default ProcessExplorer; 84 | -------------------------------------------------------------------------------- /ui-app/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /files.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | type FileInfo struct { 14 | Name string `json:"name"` 15 | Path string `json:"path"` 16 | IsDir bool `json:"isDir"` 17 | Size *int64 `json:"size,omitempty"` // in bytes 18 | Time string `json:"time"` 19 | Timestamp int64 `json:"timestamp"` 20 | } 21 | 22 | // drwxrwxrwt 1 root root 4096 2021-09-24 12:08 tmp 23 | var timeRegex = regexp.MustCompile("^(?P.+?)\\s+(?P\\d{4,4})\\-(?P\\d{2,2})\\-(?P\\d{2,2})\\s+(?P\\d{2,2})\\:(?P\\d{2,2})[^\\s]*(\\s+\\+\\d{4,4})?\\s+(?P.+)$") 24 | var spaceRegex = regexp.MustCompile("\\s+") 25 | 26 | func GetFiles(podName string, containerName string, path string, namespace string, token string) ([]FileInfo, error) { 27 | 28 | cmd := []string{"ls", "-ALl", "--full-time", "--color=never", path} 29 | buffer, err := execCmd(podName, containerName, namespace, token, cmd) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | var files []FileInfo 35 | // drwxrwxrwt 1 root root 4096 2021-09-24 12:08 tmp 36 | scanner := bufio.NewScanner(bytes.NewReader(buffer)) 37 | for scanner.Scan() { 38 | line := scanner.Text() 39 | matchedStrings := timeRegex.FindStringSubmatch(line) 40 | 41 | if len(matchedStrings) > 7 { 42 | mp := make(map[string]string) 43 | for i, name := range timeRegex.SubexpNames() { 44 | if i != 0 && name != "" { 45 | mp[name] = matchedStrings[i] 46 | } 47 | } 48 | 49 | path = strings.TrimRight(path, "\\/") 50 | fileinfo := FileInfo{ 51 | Name: mp["filename"], 52 | Path: fmt.Sprintf("%s/%s", path, mp["filename"]), 53 | Time: fmt.Sprintf("%s-%s-%s %s:%s", mp["year"], mp["month"], mp["day"], mp["hour"], mp["minute"]), 54 | } 55 | 56 | year, _ := strconv.Atoi(mp["year"]) 57 | month, _ := strconv.Atoi(mp["month"]) 58 | day, _ := strconv.Atoi(mp["day"]) 59 | hour, _ := strconv.Atoi(mp["hour"]) 60 | minute, _ := strconv.Atoi(mp["minute"]) 61 | 62 | datetime := time.Date(year, time.Month(month), day, hour, minute, 0, 0, time.UTC) 63 | fileinfo.Timestamp = datetime.Unix() 64 | 65 | splittedStrings := spaceRegex.Split(mp["other"], -1) 66 | if len(splittedStrings) > 3 { 67 | if strings.HasPrefix(splittedStrings[0], "d") { 68 | fileinfo.IsDir = true 69 | } else { 70 | size, err := strconv.ParseInt(splittedStrings[len(splittedStrings)-1], 10, 64) 71 | if err == nil { 72 | fileinfo.Size = &size 73 | } 74 | } 75 | } 76 | 77 | files = append(files, fileinfo) 78 | } else { 79 | fmt.Println("Unable to parse `ls` response :", line) 80 | } 81 | 82 | } 83 | 84 | return files, nil 85 | } 86 | 87 | func DownloadSingleFile(podName string, containerName string, path string, namespace string, token string) (*StdoutChannel, error) { 88 | 89 | cmd := []string{"cat", path} 90 | return execCmdToChannel(podName, containerName, namespace, token, cmd) 91 | } 92 | 93 | func GetFileContent(podName string, containerName string, path string, namespace string, token string) (string, error) { 94 | 95 | cmd := []string{"cat", path} 96 | return execCmdToFile(podName, containerName, namespace, token, cmd) 97 | } 98 | -------------------------------------------------------------------------------- /ui-app/src/FileViewer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useContext } from 'react'; 2 | import AceEditor from "react-ace"; 3 | import { K8sToken } from './utils' 4 | import "ace-builds/src-noconflict/mode-text"; 5 | import "ace-builds/src-noconflict/mode-json"; 6 | import "ace-builds/src-noconflict/mode-javascript"; 7 | import "ace-builds/src-noconflict/mode-ini"; 8 | import "ace-builds/src-noconflict/mode-yaml"; 9 | import "ace-builds/src-noconflict/mode-xml"; 10 | import "ace-builds/src-noconflict/mode-html"; 11 | import "ace-builds/src-noconflict/mode-css"; 12 | import "ace-builds/src-noconflict/mode-sh"; 13 | import "ace-builds/src-noconflict/theme-monokai"; 14 | 15 | 16 | 17 | export interface IFileViewerComponentProps { 18 | k8sNamespace : string, 19 | podName : string, 20 | containerName : string, 21 | filePath : string, 22 | } 23 | 24 | 25 | const FileViewer: React.FunctionComponent = ({ podName, containerName, filePath, k8sNamespace }) => { 26 | 27 | const k8sToken = useContext(K8sToken); 28 | const [content, setContent] = useState(""); 29 | const [mode, setMode] = useState("text"); 30 | 31 | useEffect( () => { 32 | let currentPodName = podName; 33 | let currencyContainerName = containerName; 34 | let currentPath = filePath; 35 | var url = (process.env.REACT_APP_BASE_URL || '').trim() + "/api/pod/" + currentPodName + '/' + currencyContainerName + '/file/view?'; 36 | url += new URLSearchParams({ 37 | token : k8sToken, 38 | namespace : k8sNamespace, 39 | path : currentPath, 40 | }).toString(); 41 | 42 | let index = filePath.lastIndexOf('.'); 43 | let extName = filePath.substr(index + 1).toLowerCase(); 44 | switch(extName) { 45 | case 'json': 46 | case 'css': 47 | case 'xml': 48 | case 'yaml': 49 | case 'sh': 50 | case 'ini': { 51 | setMode(extName); 52 | break; 53 | } 54 | case 'config': setMode('xml'); break; 55 | case 'yml': setMode('yaml'); break; 56 | case 'js': setMode('javascript'); break; 57 | default: setMode('text'); break; 58 | } 59 | 60 | const fetchData = async () => { 61 | try { 62 | const response = await fetch(url, {mode:'cors'}); 63 | var text = await response.text(); 64 | if(currentPodName !== podName || currencyContainerName !== containerName || currentPath !== filePath){ 65 | return; // response is not for the current UI 66 | } 67 | // escape \033[99m 68 | 69 | if(text){ 70 | const regex = /\033[^m\n]{1,20}m/g; 71 | text = text.replaceAll(regex, ''); // replace escapsed letters 72 | } 73 | setContent(text); 74 | 75 | } catch (error) { 76 | setContent("Error : " + error); 77 | } 78 | }; 79 | fetchData(); 80 | }, [podName, containerName, filePath, k8sToken, k8sNamespace]); 81 | 82 | return ( 83 | 105 | ); 106 | }; 107 | 108 | 109 | export default FileViewer; 110 | -------------------------------------------------------------------------------- /ui-app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useCallback, useMemo, useState, useEffect } from 'react'; 3 | import { TextField } from '@fluentui/react/lib/TextField'; 4 | import { useBoolean } from '@fluentui/react-hooks'; 5 | import { Dialog, DialogType, DialogFooter } from '@fluentui/react/lib/Dialog'; 6 | import { PrimaryButton, DefaultButton } from '@fluentui/react/lib/Button'; 7 | import { 8 | Link, 9 | MessageBar, 10 | MessageBarType, 11 | } from '@fluentui/react'; 12 | import { 13 | HashRouter as Router, 14 | Switch, 15 | Route, 16 | Redirect, 17 | } from 'react-router-dom'; 18 | import { mergeStyleSets } from '@fluentui/react/lib/Styling'; 19 | import { getStorageValue, useLocalStorage, K8sToken, K8sNamespace } from './utils' 20 | import PodList from './PodList'; 21 | import FileViewerWrapper from './FileViewerWrapper'; 22 | import { VERSION } from './version'; 23 | import './App.css'; 24 | 25 | 26 | 27 | const classNames = mergeStyleSets({ 28 | pageHeader: { 29 | width: '100vw', 30 | height: '68px', 31 | flex: '1 1 auto', 32 | display: 'flex', 33 | alignItems: 'center', 34 | overflow: 'hidden', 35 | boxShadow: '0 0px 6px 4px #999', 36 | marginBottom: '10px', 37 | position: 'relative', 38 | }, 39 | logo : { 40 | width: '48px', 41 | height: '48px', 42 | marginLeft: '10px', 43 | }, 44 | pageHeaderTitle: { 45 | marginLeft: '10px', 46 | fontSize : '32px' 47 | }, 48 | links: { 49 | position: 'absolute', 50 | right: '30px', 51 | bottom: '10px' 52 | }, 53 | versionLink : { 54 | fontSize : '12px' 55 | } 56 | }); 57 | 58 | const dialogStyles = { main: { maxWidth: 1024, minWidth: 800 } }; 59 | const dialogContentProps = { 60 | type: DialogType.normal, 61 | title: 'Settings', 62 | closeButtonAriaLabel: 'Close', 63 | }; 64 | 65 | declare global { 66 | interface Window { 67 | POD_NAMESPACE:any; 68 | } 69 | } 70 | 71 | const DEFAULT_NS = window.POD_NAMESPACE || ''; 72 | 73 | 74 | 75 | function App() { 76 | 77 | const [hideDialog, { toggle: toggleHideDialog }] = useBoolean(true); 78 | const [token, setToken] = useState(getStorageValue('K8S_TOKEN', "")); 79 | const [namespace, setNamespace] = useState(getStorageValue('K8S_NAMESPACE', DEFAULT_NS)); 80 | const [persistentToken, setPersistentToken] = useLocalStorage("K8S_TOKEN", ""); 81 | const [persistentNamespace, setPersistentNamespace] = useLocalStorage("K8S_NAMESPACE", DEFAULT_NS); 82 | 83 | const modalProps = useMemo( 84 | () => ({ 85 | 86 | isBlocking: true, 87 | styles: dialogStyles, 88 | }), 89 | [], 90 | ); 91 | 92 | 93 | 94 | useEffect( () => { 95 | if( !persistentToken && !persistentNamespace ) { 96 | toggleHideDialog(); 97 | } 98 | }, [toggleHideDialog]); 99 | 100 | 101 | const handleTokenChange = (e : any, value : string | undefined) => { 102 | setToken(value || ""); 103 | }; 104 | const handleNamespaceChange = (e : any, value : string | undefined) => { 105 | setNamespace(value || ""); 106 | }; 107 | 108 | const saveSettings = useCallback( () => { 109 | setPersistentToken(token); 110 | setPersistentNamespace(namespace); 111 | toggleHideDialog(); 112 | }, [token, namespace, toggleHideDialog, setPersistentToken, setPersistentNamespace]); 113 | 114 | return ( 115 | 116 | 117 |
118 | K8S Logo 119 |
Kubernetes Pod Inspector {VERSION}
120 |
Namespace :   121 | 122 | {namespace ? namespace : 'default' } 123 | 124 |
125 |
126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 165 |
166 | 167 | ); 168 | } 169 | 170 | 171 | 172 | 173 | 174 | export default App; 175 | -------------------------------------------------------------------------------- /pods.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "math" 6 | "time" 7 | 8 | corev1 "k8s.io/api/core/v1" 9 | _ "k8s.io/apimachinery/pkg/api/errors" 10 | "k8s.io/apimachinery/pkg/api/resource" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | _ "k8s.io/apimachinery/pkg/runtime" 13 | "k8s.io/client-go/kubernetes" 14 | _ "k8s.io/client-go/tools/remotecommand" 15 | metrics "k8s.io/metrics/pkg/client/clientset/versioned" 16 | // 17 | // Uncomment to load all auth plugins 18 | // _ "k8s.io/client-go/plugin/pkg/client/auth" 19 | // 20 | // Or uncomment to load specific auth plugins 21 | // _ "k8s.io/client-go/plugin/pkg/client/auth/azure" 22 | // _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 23 | // _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" 24 | // _ "k8s.io/client-go/plugin/pkg/client/auth/openstack" 25 | ) 26 | 27 | type K8sContainer struct { 28 | Ready bool `json:"ready"` 29 | RestartCount int32 `json:"restartCount"` 30 | CpuUsage int64 `json:"cpuUsage"` // 1 Core = 1000 milli 31 | CpuLimit int64 `json:"cpuLimit"` // 1 Core = 1000 milli 32 | 33 | RamUsage int64 `json:"ramUsage"` // KB 34 | RamLimit int64 `json:"ramLimit"` // KB 35 | 36 | } 37 | 38 | type K8sPod struct { 39 | Name string `json:"name"` 40 | Status corev1.PodPhase `json:"status"` 41 | Age int64 `json:"age"` 42 | Ready int64 `json:"ready"` 43 | HostIp string `json:"hostIp"` 44 | PodIp string `json:"podIp"` 45 | CpuUsage int64 `json:"cpuUsage"` // 1 Core = 1000 milli 46 | CpuLimit int64 `json:"cpuLimit"` // 1 Core = 1000 milli 47 | CpuPercentage float32 `json:"cpuPercentage"` 48 | RamUsage int64 `json:"ramUsage"` // KB 49 | RamLimit int64 `json:"ramLimit"` // KB 50 | RamPercentage float32 `json:"ramPercentage"` 51 | Containers map[string]K8sContainer `json:"containers"` 52 | } 53 | 54 | func GetPods(namespace string, token string) ([]K8sPod, error) { 55 | podMap := make(map[string]*K8sPod) 56 | 57 | config, err := loadConfig(token) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | // create clientset 63 | clientset, err := kubernetes.NewForConfig(config) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | // get pod list 69 | pl, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{}) 70 | if err != nil { 71 | return nil, err 72 | } 73 | for _, item := range pl.Items { 74 | metadata := item.GetObjectMeta() 75 | if metadata != nil { 76 | pod := K8sPod{ 77 | Name: metadata.GetName(), 78 | Status: item.Status.Phase, 79 | HostIp: item.Status.HostIP, 80 | PodIp: item.Status.PodIP, 81 | Containers: make(map[string]K8sContainer), 82 | Ready: 0, 83 | CpuUsage: -1, 84 | RamUsage: -1, 85 | } 86 | 87 | if item.Status.StartTime != nil { 88 | pod.Age = int64(time.Now().UTC().Sub(item.Status.StartTime.UTC()).Seconds()) 89 | } 90 | 91 | for _, cs := range item.Status.ContainerStatuses { 92 | pod.Containers[cs.Name] = K8sContainer{ 93 | Ready: cs.Ready, 94 | RestartCount: cs.RestartCount, 95 | CpuUsage: -1, 96 | RamUsage: -1, 97 | } 98 | if cs.Ready { 99 | pod.Ready++ 100 | } 101 | } 102 | 103 | for _, c := range item.Spec.Containers { 104 | if container, ok := pod.Containers[c.Name]; ok { 105 | cpuLimit := c.Resources.Limits.Cpu() 106 | if cpuLimit != nil { 107 | container.CpuLimit = cpuLimit.ScaledValue(resource.Milli) 108 | pod.CpuLimit += container.CpuLimit 109 | } 110 | ramLimit := c.Resources.Limits.Memory() 111 | if ramLimit != nil { 112 | container.RamLimit = ramLimit.ScaledValue(resource.Kilo) 113 | pod.RamLimit += container.RamLimit 114 | } 115 | pod.Containers[c.Name] = container 116 | } 117 | } 118 | podMap[metadata.GetName()] = &pod 119 | } 120 | } 121 | 122 | // metrics clientset 123 | mc, err := metrics.NewForConfig(config) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | // get metrics 129 | pml, err := mc.MetricsV1beta1().PodMetricses(namespace).List(context.TODO(), metav1.ListOptions{}) 130 | if err == nil { 131 | // merge into result 132 | for _, item := range pml.Items { 133 | metadata := item.GetObjectMeta() 134 | if metadata != nil { 135 | if pod, ok := podMap[metadata.GetName()]; ok { 136 | 137 | for _, c := range item.Containers { 138 | if container, ok := pod.Containers[c.Name]; ok { 139 | cpuUsage := c.Usage.Cpu() 140 | if cpuUsage != nil { 141 | container.CpuUsage = cpuUsage.ScaledValue(resource.Milli) 142 | if pod.CpuUsage < 0 { 143 | pod.CpuUsage = 0 144 | } 145 | pod.CpuUsage += container.CpuUsage 146 | 147 | if pod.CpuLimit > 0 { 148 | percentage := float64(pod.CpuUsage) / (float64(pod.CpuLimit) * float64(1.0)) 149 | pod.CpuPercentage = float32(math.Round(percentage*1000)) / float32(1000.0) 150 | } 151 | } 152 | ramUsage := c.Usage.Memory() 153 | if ramUsage != nil { 154 | container.RamUsage = ramUsage.ScaledValue(resource.Kilo) 155 | if pod.RamUsage < 0 { 156 | pod.RamUsage = 0 157 | } 158 | pod.RamUsage += container.RamUsage 159 | 160 | if pod.RamLimit > 0 { 161 | percentage := float64(pod.RamUsage) / (float64(pod.RamLimit) * float64(1.0)) 162 | pod.RamPercentage = float32(math.Round(percentage*1000)) / float32(1000.0) 163 | } 164 | } 165 | pod.Containers[c.Name] = container 166 | } 167 | } 168 | } 169 | } 170 | } 171 | } // if err == nil 172 | 173 | pods := make([]K8sPod, 0, len(podMap)) 174 | for _, pod := range podMap { 175 | pods = append(pods, *pod) 176 | } 177 | 178 | return pods, nil 179 | } 180 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "path/filepath" 11 | "time" 12 | 13 | "github.com/gin-contrib/cors" 14 | "github.com/gin-gonic/gin" 15 | ) 16 | 17 | func main() { 18 | 19 | var username, password, uiPath string 20 | port := *flag.Int("port", 8080, "HTTP port to listen") 21 | 22 | flag.StringVar(&username, "user", "", "Username to enable basic-authentication") 23 | flag.StringVar(&password, "password", "", "Password to enable basic-authentication") 24 | flag.StringVar(&uiPath, "ui-path", "./www/", "Path of static web sites") 25 | flag.Parse() 26 | 27 | gin.SetMode(gin.ReleaseMode) 28 | router := gin.Default() 29 | 30 | var r *gin.RouterGroup 31 | if len(username) > 0 && len(password) > 0 { 32 | r = router.Group("/", gin.BasicAuth(gin.Accounts{ 33 | username: password, 34 | })) 35 | } else { 36 | r = router.Group("/") 37 | } 38 | 39 | // allow CORS request from localhost 40 | r.Use(cors.New(cors.Config{ 41 | AllowMethods: []string{"PUT", "PATCH", "GET", "POST", "DELETE"}, 42 | AllowHeaders: []string{"Origin"}, 43 | ExposeHeaders: []string{"Content-Length"}, 44 | AllowCredentials: true, 45 | AllowOriginFunc: func(origin string) bool { 46 | if u, err := url.Parse(origin); err == nil { 47 | hostname := u.Hostname() 48 | return hostname == "localhost" || hostname == "127.0.0.1" 49 | } 50 | return false 51 | }, 52 | MaxAge: time.Minute, 53 | })) 54 | 55 | r.GET("/env", getEnv) 56 | r.GET("/api/pods", getPods) 57 | r.GET("/api/pod/:pod/:container/file/list", getFiles) 58 | r.GET("/api/pod/:pod/:container/file/view", viewFile) 59 | r.GET("/api/pod/:pod/:container/file/download", downloadFile) 60 | r.GET("/api/pod/:pod/:container/process/list", getProcesses) 61 | 62 | dirPath, err := filepath.Abs(uiPath) 63 | if err != nil { 64 | panic(err) 65 | } 66 | 67 | files, err := ioutil.ReadDir(dirPath) 68 | if err != nil { 69 | panic(err) 70 | } 71 | 72 | // static web sites 73 | for _, f := range files { 74 | if f.IsDir() { 75 | if f.Name() != "." && f.Name() != ".." { 76 | r.Static("/"+f.Name()+"/", filepath.Join(dirPath, f.Name())) 77 | } 78 | } else { 79 | 80 | r.StaticFile("/"+f.Name(), filepath.Join(dirPath, f.Name())) 81 | } 82 | } 83 | r.StaticFile("/", filepath.Join(dirPath, "index.html")) // default page 84 | 85 | fmt.Printf("HTTP listening on port %d\n", port) 86 | panic(router.Run(fmt.Sprintf(":%d", port))) 87 | } 88 | 89 | func getEnv(c *gin.Context) { 90 | script := fmt.Sprintf("window.POD_NAMESPACE='%s';", os.Getenv("POD_NAMESPACE")) 91 | 92 | c.Data(http.StatusOK, "text/javascript", []byte(script)) 93 | } 94 | 95 | func getPods(c *gin.Context) { 96 | namespace := c.Query("namespace") 97 | token := c.Query("token") 98 | pods, err := GetPods(namespace, token) 99 | if err != nil { 100 | c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) 101 | } else { 102 | c.JSON(http.StatusOK, pods) 103 | } 104 | } 105 | 106 | func getFiles(c *gin.Context) { 107 | podName := c.Param("pod") 108 | containerName := c.Param("container") 109 | path := c.DefaultQuery("path", "/") 110 | namespace := c.Query("namespace") 111 | token := c.Query("token") 112 | files, err := GetFiles(podName, containerName, path, namespace, token) 113 | if err != nil { 114 | c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) 115 | } else { 116 | c.JSON(http.StatusOK, files) 117 | } 118 | } 119 | 120 | func downloadFile(c *gin.Context) { 121 | podName := c.Param("pod") 122 | containerName := c.Param("container") 123 | path := c.DefaultQuery("path", "/") 124 | namespace := c.Query("namespace") 125 | token := c.Query("token") 126 | 127 | stdout, err := DownloadSingleFile(podName, containerName, path, namespace, token) 128 | if err != nil { 129 | c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) 130 | return 131 | } 132 | defer stdout.Close() 133 | 134 | _, filename := filepath.Split(path) 135 | 136 | w := c.Writer 137 | header := w.Header() 138 | header.Set("Content-Description", "File Transfer") 139 | header.Set("Content-Transfer-Encoding", "binary") 140 | header.Set("Content-Disposition", "attachment; filename="+filename) 141 | header.Set("Content-Type", "application/octet-stream") 142 | header.Set("Transfer-Encoding", "chunked") 143 | w.WriteHeader(http.StatusOK) 144 | 145 | for { 146 | select { 147 | case bufOrErr := <-stdout.Channel(): 148 | { 149 | if bufOrErr.err != nil { 150 | fmt.Println("Unable to download file", path, bufOrErr.err) 151 | goto lbExit 152 | } 153 | if bufOrErr.buf == nil { 154 | goto lbExit 155 | } 156 | _, err = w.Write(bufOrErr.buf) 157 | if err != nil { 158 | goto lbExit 159 | } 160 | } 161 | 162 | case _ = <-time.After(10 * time.Second): 163 | _, err = w.Write([]byte{}) 164 | if err != nil { 165 | goto lbExit 166 | } 167 | w.(http.Flusher).Flush() 168 | } 169 | } 170 | lbExit: 171 | w.(http.Flusher).Flush() 172 | 173 | } 174 | 175 | func viewFile(c *gin.Context) { 176 | podName := c.Param("pod") 177 | containerName := c.Param("container") 178 | path := c.DefaultQuery("path", "/") 179 | namespace := c.Query("namespace") 180 | token := c.Query("token") 181 | 182 | tempFile, err := GetFileContent(podName, containerName, path, namespace, token) 183 | if err != nil { 184 | c.JSON(http.StatusInternalServerError, err.Error()) 185 | return 186 | } 187 | defer os.Remove(tempFile) 188 | 189 | _, filename := filepath.Split(path) 190 | 191 | c.Header("Content-Description", "File Transfer") 192 | c.Header("Content-Transfer-Encoding", "binary") 193 | c.Header("Content-Disposition", "attachment; filename="+filename) 194 | c.Header("Content-Type", "application/octet-stream") 195 | c.File(tempFile) 196 | 197 | } 198 | 199 | func getProcesses(c *gin.Context) { 200 | podName := c.Param("pod") 201 | containerName := c.Param("container") 202 | namespace := c.Query("namespace") 203 | token := c.Query("token") 204 | result, err := GetPsResult(podName, containerName, namespace, token) 205 | if err != nil { 206 | c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) 207 | } else { 208 | c.JSON(http.StatusOK, map[string]string{"result": string(result)}) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Pod Inspector 2 | 3 | Unlike other dashboardes for Kubernetes(Lens / Rancher / etc), Kubernetes Pod Inspector allows to check the file system and processes within running Linux pods without using kubectl. 4 | This is useful when we want to check the files within volumes mounted by pods 5 | 6 | 7 | ## How to Deploy 8 | 9 | 10 | The docker image is available at [docker.io/wangjia184/pod-inspector](https://hub.docker.com/repository/docker/wangjia184/pod-inspector). 11 | Typically, it can be deployed into K8S cluster with following yaml. 12 | 13 | ```yaml 14 | apiVersion: apps/v1 15 | kind: Deployment 16 | metadata: 17 | name: pod-inspector-deployment 18 | labels: 19 | app: pod-inspector 20 | spec: 21 | replicas: 1 22 | selector: 23 | matchLabels: 24 | app: pod-inspector 25 | template: 26 | metadata: 27 | labels: 28 | app: pod-inspector 29 | spec: 30 | containers: 31 | - name: pod-inspector 32 | image: docker.io/wangjia184/pod-inspector:latest 33 | args: ["-port", "8080", "-user", "", "-password", ""] 34 | ports: 35 | - containerPort: 8080 36 | env: 37 | - name: K8S_NODE_NAME 38 | valueFrom: 39 | fieldRef: 40 | fieldPath: spec.nodeName 41 | - name: NODE_IP 42 | valueFrom: 43 | fieldRef: 44 | fieldPath: status.hostIP 45 | - name: POD_NAME 46 | valueFrom: 47 | fieldRef: 48 | fieldPath: metadata.labels['statefulset.kubernetes.io/pod-name'] 49 | - name: POD_NAMESPACE 50 | valueFrom: 51 | fieldRef: 52 | fieldPath: metadata.namespace 53 | - name: POD_IP 54 | valueFrom: 55 | fieldRef: 56 | fieldPath: status.podIP 57 | - name: POD_SERVICE_ACCOUNT 58 | valueFrom: 59 | fieldRef: 60 | fieldPath: spec.serviceAccountName 61 | imagePullPolicy: Always 62 | ``` 63 | 64 | It listens on port 8080 for HTTP service. You can specify `user` and `password` in arguments to enable http authentication. 65 | 66 | Next, expose port 8080 so that you can access it. Here is an example: 67 | 68 | ```yaml 69 | apiVersion: v1 70 | kind: Service 71 | metadata: 72 | name: pod-inspector 73 | spec: 74 | selector: 75 | app: pod-inspector 76 | ports: 77 | - name: http 78 | protocol: TCP 79 | port: 80 80 | targetPort: 8080 81 | type: ClusterIP 82 | --- 83 | apiVersion: networking.k8s.io/v1 84 | kind: Ingress 85 | metadata: 86 | name: pod-inspector 87 | spec: 88 | rules: 89 | - host: your.kubernetes.cluster.domain-name.local 90 | http: 91 | paths: 92 | - path: / 93 | pathType: Prefix 94 | backend: 95 | service: 96 | name: pod-inspector 97 | port: 98 | number: 80 99 | 100 | ``` 101 | 102 | Finally you should be able to access the web site. By filling the token and namespace of K8S cluster, it will connect to the same cluster the pod is running. Token can be retrieved in `~/.kube/config` file. 103 | 104 | ![](0.png) 105 | 106 | ## Use a dedicated service account 107 | 108 | If your cluster uses RBAC, you can also run the inspector with a dedicated service account and grant proper roles in order to use in-cluster token assigned from the service account. 109 | Here is an example to create the service account and its role. 110 | 111 | ```yaml 112 | apiVersion: v1 113 | kind: ServiceAccount 114 | metadata: 115 | name: pod-inspector 116 | automountServiceAccountToken: true 117 | --- 118 | apiVersion: rbac.authorization.k8s.io/v1 119 | kind: Role 120 | metadata: 121 | name: pod-inspector 122 | rules: 123 | - apiGroups: [""] # "" indicates the core API group 124 | resources: ["pods"] 125 | verbs: ["get", "watch", "list"] 126 | - apiGroups: [""] 127 | resources: ["pods/exec"] 128 | verbs: ["create"] 129 | - apiGroups: ["metrics.k8s.io"] 130 | resources: ["pods", "nodes"] 131 | verbs: ["get", "watch", "list"] 132 | --- 133 | apiVersion: rbac.authorization.k8s.io/v1 134 | kind: RoleBinding 135 | metadata: 136 | name: inspect-pods 137 | subjects: 138 | - kind: ServiceAccount 139 | name: pod-inspector # service account name 140 | roleRef: 141 | kind: Role 142 | name: pod-inspector # role name 143 | apiGroup: rbac.authorization.k8s.io 144 | ``` 145 | 146 | Then specifiy the service account in your pod. 147 | 148 | ```yaml 149 | apiVersion: apps/v1 150 | kind: Deployment 151 | metadata: 152 | name: pod-inspector-deployment 153 | labels: 154 | app: pod-inspector 155 | spec: 156 | replicas: 1 157 | selector: 158 | matchLabels: 159 | app: pod-inspector 160 | template: 161 | metadata: 162 | labels: 163 | app: pod-inspector 164 | spec: 165 | serviceAccountName: pod-inspector # service account 166 | containers: 167 | - name: pod-inspector 168 | image: docker.io/wangjia184/pod-inspector:latest 169 | args: ["-port", "8080", "-user", "admin", "-password", "654321"] 170 | ports: 171 | - containerPort: 8080 172 | env: 173 | - name: K8S_NODE_NAME 174 | valueFrom: 175 | fieldRef: 176 | fieldPath: spec.nodeName 177 | - name: NODE_IP 178 | valueFrom: 179 | fieldRef: 180 | fieldPath: status.hostIP 181 | - name: POD_NAME 182 | valueFrom: 183 | fieldRef: 184 | fieldPath: metadata.labels['statefulset.kubernetes.io/pod-name'] 185 | - name: POD_NAMESPACE 186 | valueFrom: 187 | fieldRef: 188 | fieldPath: metadata.namespace 189 | - name: POD_IP 190 | valueFrom: 191 | fieldRef: 192 | fieldPath: status.podIP 193 | - name: POD_SERVICE_ACCOUNT 194 | valueFrom: 195 | fieldRef: 196 | fieldPath: spec.serviceAccountName 197 | imagePullPolicy: Always 198 | ``` 199 | 200 | ## Screenshots 201 | 202 | 203 | ### List of Pods 204 | ![List of Pods](1.png) 205 | 206 | ### Files within Pods 207 | ![Files within Pods](2.png) 208 | 209 | ### View File (or you can download as well) 210 | ![](3.png) 211 | 212 | ### Check Processes 213 | ![](4.png) 214 | 215 | -------------------------------------------------------------------------------- /k8s.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | 12 | corev1 "k8s.io/api/core/v1" 13 | _ "k8s.io/apimachinery/pkg/api/errors" 14 | _ "k8s.io/apimachinery/pkg/api/resource" 15 | _ "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | "k8s.io/apimachinery/pkg/runtime" 17 | "k8s.io/client-go/kubernetes" 18 | "k8s.io/client-go/rest" 19 | "k8s.io/client-go/tools/clientcmd" 20 | "k8s.io/client-go/tools/remotecommand" 21 | "k8s.io/client-go/util/homedir" 22 | _ "k8s.io/metrics/pkg/client/clientset/versioned" 23 | // 24 | // Uncomment to load all auth plugins 25 | // _ "k8s.io/client-go/plugin/pkg/client/auth" 26 | // 27 | // Or uncomment to load specific auth plugins 28 | // _ "k8s.io/client-go/plugin/pkg/client/auth/azure" 29 | // _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 30 | // _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" 31 | // _ "k8s.io/client-go/plugin/pkg/client/auth/openstack" 32 | ) 33 | 34 | func loadConfig(token string) (*rest.Config, error) { 35 | 36 | var kubeconfig string 37 | if home := homedir.HomeDir(); home != "" { 38 | kubeconfig = filepath.Join(home, ".kube", "config") 39 | if _, err := os.Stat(kubeconfig); os.IsNotExist(err) { 40 | fmt.Println("Local config file %s does not exist", kubeconfig) 41 | } else { 42 | config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) 43 | if err == nil { 44 | if len(token) > 0 { 45 | config.BearerToken = token 46 | } 47 | return config, nil 48 | } 49 | } 50 | } 51 | config, err := rest.InClusterConfig() 52 | if err == nil { 53 | if len(token) > 0 { 54 | config.BearerToken = token 55 | } 56 | return config, nil 57 | } 58 | 59 | return nil, errors.New(fmt.Sprintf("Failed to load config from %s; Failed to load in-cluster config", kubeconfig)) 60 | } 61 | 62 | // sizeQueue implements remotecommand.TerminalSizeQueue 63 | type fixedTerminalSizeQueue struct { 64 | } 65 | 66 | // make sure sizeQueue implements the resize.TerminalSizeQueue interface 67 | var _ remotecommand.TerminalSizeQueue = &fixedTerminalSizeQueue{} 68 | 69 | func (s *fixedTerminalSizeQueue) Next() *remotecommand.TerminalSize { 70 | size := remotecommand.TerminalSize{ 71 | Width: 3000, 72 | Height: 8000, 73 | } 74 | return &size 75 | } 76 | 77 | func execCmd(podName string, containerName string, namespace string, token string, cmd []string) ([]byte, error) { 78 | config, err := loadConfig(token) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | // create the clientset 84 | clientset, err := kubernetes.NewForConfig(config) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | // https://github.com/kubernetes/kubernetes/blob/release-1.22/test/e2e/framework/exec_util.go 90 | // https://zhimin-wen.medium.com/programing-exec-into-a-pod-5f2a70bd93bb 91 | req := clientset.CoreV1(). 92 | RESTClient(). 93 | Post(). 94 | Resource("pods"). 95 | Name(podName). 96 | Namespace(namespace). 97 | SubResource("exec"). 98 | Param("container", containerName) 99 | 100 | scheme := runtime.NewScheme() 101 | if err := corev1.AddToScheme(scheme); err != nil { 102 | return nil, err 103 | } 104 | 105 | parameterCodec := runtime.NewParameterCodec(scheme) 106 | req.VersionedParams(&corev1.PodExecOptions{ 107 | Stdin: false, 108 | Stdout: true, 109 | Stderr: true, 110 | TTY: false, 111 | Container: podName, 112 | Command: cmd, 113 | }, parameterCodec) 114 | 115 | exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL()) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | var stdout, stderr bytes.Buffer 121 | err = exec.Stream(remotecommand.StreamOptions{ 122 | Stdin: nil, 123 | Stdout: &stdout, 124 | Stderr: &stderr, 125 | TerminalSizeQueue: &fixedTerminalSizeQueue{}, 126 | }) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | return stdout.Bytes(), nil 132 | } 133 | 134 | func execCmdToFile(podName string, containerName string, namespace string, token string, cmd []string) (string, error) { 135 | config, err := loadConfig(token) 136 | if err != nil { 137 | return "", err 138 | } 139 | 140 | // create the clientset 141 | clientset, err := kubernetes.NewForConfig(config) 142 | if err != nil { 143 | return "", err 144 | } 145 | 146 | // https://github.com/kubernetes/kubernetes/blob/release-1.22/test/e2e/framework/exec_util.go 147 | // https://zhimin-wen.medium.com/programing-exec-into-a-pod-5f2a70bd93bb 148 | req := clientset.CoreV1(). 149 | RESTClient(). 150 | Post(). 151 | Resource("pods"). 152 | Name(podName). 153 | Namespace(namespace). 154 | SubResource("exec"). 155 | Param("container", containerName) 156 | 157 | scheme := runtime.NewScheme() 158 | if err := corev1.AddToScheme(scheme); err != nil { 159 | return "", err 160 | } 161 | 162 | parameterCodec := runtime.NewParameterCodec(scheme) 163 | req.VersionedParams(&corev1.PodExecOptions{ 164 | Stdin: false, 165 | Stdout: true, 166 | Stderr: false, 167 | TTY: false, 168 | Container: podName, 169 | Command: cmd, 170 | }, parameterCodec) 171 | 172 | exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL()) 173 | if err != nil { 174 | return "", err 175 | } 176 | 177 | // create temp file 178 | tempFile, err := ioutil.TempFile("", "k8s_cmd") 179 | if err != nil { 180 | return "", err 181 | } 182 | 183 | err = exec.Stream(remotecommand.StreamOptions{ 184 | Stdin: nil, 185 | Stdout: tempFile, 186 | Stderr: nil, 187 | TerminalSizeQueue: &fixedTerminalSizeQueue{}, 188 | }) 189 | if err != nil { 190 | os.Remove(tempFile.Name()) 191 | return "", err 192 | } 193 | 194 | err = tempFile.Sync() 195 | if err != nil { 196 | os.Remove(tempFile.Name()) 197 | return "", err 198 | } 199 | 200 | defer tempFile.Close() 201 | 202 | return tempFile.Name(), nil 203 | } 204 | 205 | type BufOrErr struct { 206 | buf []byte 207 | err error 208 | } 209 | type StdoutChannel struct { 210 | channel chan BufOrErr 211 | closed bool 212 | } 213 | 214 | func (self *StdoutChannel) Close() { 215 | self.closed = true 216 | } 217 | 218 | func (self *StdoutChannel) Channel() chan BufOrErr { 219 | return self.channel 220 | } 221 | 222 | func (self *StdoutChannel) Write(data []byte) (n int, err error) { 223 | if self.closed { 224 | return 0, io.ErrClosedPipe 225 | } 226 | n = len(data) 227 | var buf = make([]byte, n) 228 | copy(buf, data) 229 | self.channel <- BufOrErr{buf, nil} 230 | return n, nil 231 | } 232 | 233 | func execCmdToChannel(podName string, containerName string, namespace string, token string, cmd []string) (*StdoutChannel, error) { 234 | config, err := loadConfig(token) 235 | if err != nil { 236 | return nil, err 237 | } 238 | 239 | // create the clientset 240 | clientset, err := kubernetes.NewForConfig(config) 241 | if err != nil { 242 | return nil, err 243 | } 244 | 245 | // https://github.com/kubernetes/kubernetes/blob/release-1.22/test/e2e/framework/exec_util.go 246 | // https://zhimin-wen.medium.com/programing-exec-into-a-pod-5f2a70bd93bb 247 | req := clientset.CoreV1(). 248 | RESTClient(). 249 | Post(). 250 | Resource("pods"). 251 | Name(podName). 252 | Namespace(namespace). 253 | SubResource("exec"). 254 | Param("container", containerName) 255 | 256 | scheme := runtime.NewScheme() 257 | if err := corev1.AddToScheme(scheme); err != nil { 258 | return nil, err 259 | } 260 | 261 | parameterCodec := runtime.NewParameterCodec(scheme) 262 | req.VersionedParams(&corev1.PodExecOptions{ 263 | Stdin: false, 264 | Stdout: true, 265 | Stderr: false, 266 | TTY: false, 267 | Container: podName, 268 | Command: cmd, 269 | }, parameterCodec) 270 | 271 | exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL()) 272 | if err != nil { 273 | return nil, err 274 | } 275 | 276 | stdout := StdoutChannel{ 277 | channel: make(chan BufOrErr, 100), 278 | closed: false, 279 | } 280 | go (func() { 281 | defer close(stdout.channel) 282 | 283 | err = exec.Stream(remotecommand.StreamOptions{ 284 | Stdin: nil, 285 | Stdout: &stdout, 286 | Stderr: nil, 287 | TerminalSizeQueue: &fixedTerminalSizeQueue{}, 288 | }) 289 | if !stdout.closed { 290 | if err != nil { 291 | stdout.channel <- BufOrErr{nil, err} 292 | } else { 293 | stdout.channel <- BufOrErr{nil, nil} 294 | } 295 | } 296 | fmt.Println("Ended.") 297 | })() 298 | 299 | return &stdout, nil 300 | } 301 | -------------------------------------------------------------------------------- /ui-app/src/PodList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback, useContext, useRef } from 'react'; 2 | import { useBoolean, useSetTimeout } from '@fluentui/react-hooks'; 3 | import { FontIcon } from '@fluentui/react/lib/Icon'; 4 | import { MessageBar, MessageBarType } from '@fluentui/react'; 5 | import { DetailsListLayoutMode, SelectionMode, IColumn } from '@fluentui/react/lib/DetailsList'; 6 | import { ShimmeredDetailsList } from '@fluentui/react/lib/ShimmeredDetailsList'; 7 | import { ProgressIndicator } from '@fluentui/react/lib/ProgressIndicator'; 8 | import { Panel, PanelType, IPanelProps } from '@fluentui/react/lib/Panel'; 9 | import { IRenderFunction } from '@fluentui/react/lib/Utilities'; 10 | import { Text } from '@fluentui/react/lib/Text'; 11 | import { Dropdown, IDropdownOption } from '@fluentui/react/lib/Dropdown'; 12 | import { Stack, IStackTokens } from '@fluentui/react'; 13 | 14 | import { mergeStyleSets } from '@fluentui/react/lib/Styling'; 15 | import { K8sToken, K8sNamespace } from './utils' 16 | import IPod from './dto' 17 | import Pod from './Pod' 18 | import './PodList.css' 19 | 20 | const classNames = mergeStyleSets({ 21 | progressBar: { 22 | '.ms-ProgressIndicator-itemName': { 23 | direction : 'ltr', 24 | display: 'flex', 25 | flexDirection: 'column', 26 | alignItems: 'flex-end', 27 | fontFamily: "monospace, 'Courier New', Courier", 28 | } 29 | }, 30 | }); 31 | 32 | 33 | 34 | export interface IPodListState { 35 | pods: IPod[]; 36 | } 37 | 38 | 39 | // sort function 40 | type FunctionType = (items: T[], columnKey: string, isSortedDescending?: boolean) => T[]; 41 | 42 | 43 | type ColumnClickHandler = (ev: React.MouseEvent, column: IColumn) => void; 44 | 45 | var timer : number = 0; 46 | 47 | const PodList: React.FunctionComponent = () => { 48 | const k8sToken = useContext(K8sToken); 49 | const k8sNamespace = useContext(K8sNamespace); 50 | const [defaultContainer, setDefaultContainer] = useState(""); 51 | const [selectedContainer, setSelectedContainer] = useState(""); 52 | const [dropDownOptions, setDropDownOptions] = useState([]); 53 | const [errorMessage, setErrorMessage] = useState(undefined); 54 | const [pods, setPods] = useState(undefined); 55 | const [selectedPod, setSelectedPod] = useState(undefined); 56 | const [isPanelOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false); 57 | const columnsHolder = useRef([]); 58 | const columnClickHandler = useRef((ev: React.MouseEvent, column: IColumn) : void => {}); 59 | const sortedColumn = useRef("name"); 60 | const sortedDescending = useRef(false); 61 | 62 | const { setTimeout, clearTimeout } = useSetTimeout(); 63 | 64 | const copyAndSort : FunctionType = (items, columnKey, isSortedDescending ) => { 65 | sortedColumn.current = columnKey; 66 | sortedDescending.current = isSortedDescending ? true : false; 67 | const key = columnKey as keyof IPod; 68 | return items.slice(0).sort((a: IPod, b: IPod) => ((isSortedDescending ? a[key] < b[key] : a[key] > b[key]) ? 1 : -1)); 69 | }; 70 | 71 | useEffect(() => { 72 | columnClickHandler.current = (ev: React.MouseEvent, column: IColumn) : void => { 73 | 74 | const currColumn: IColumn = columnsHolder.current.filter( 75 | (currCol) => column.key === currCol.key 76 | )[0]; 77 | columnsHolder.current.forEach((col: IColumn) => { 78 | if (col === currColumn) { 79 | col.isSortedDescending = !col.isSortedDescending; 80 | col.isSorted = true; 81 | } else { 82 | col.isSorted = false; 83 | col.isSortedDescending = true; 84 | } 85 | }); 86 | 87 | if (pods != null) { 88 | setPods( 89 | copyAndSort( 90 | pods, 91 | currColumn.fieldName!, 92 | currColumn.isSortedDescending 93 | ) 94 | ); 95 | } 96 | 97 | }; 98 | }, [pods]); 99 | 100 | 101 | // initialize columns 102 | useEffect(() => { 103 | columnsHolder.current = [ 104 | { 105 | key: 'pod-name', 106 | name: 'Pod Name', 107 | fieldName: 'name', 108 | minWidth: 210, 109 | maxWidth: 450, 110 | isRowHeader: true, 111 | isResizable: true, 112 | isSorted: true, 113 | isSortedDescending: false, 114 | sortAscendingAriaLabel: 'Sorted A to Z', 115 | sortDescendingAriaLabel: 'Sorted Z to A', 116 | onColumnClick: (ev: React.MouseEvent, column: IColumn) : void => { columnClickHandler.current(ev, column); }, 117 | data: 'string', 118 | isPadded: true, 119 | }, 120 | { 121 | key: 'pod-status', 122 | name: 'Status', 123 | fieldName: 'status', 124 | minWidth: 70, 125 | maxWidth: 100, 126 | isResizable: true, 127 | onColumnClick: (ev: React.MouseEvent, column: IColumn) : void => { columnClickHandler.current(ev, column); }, 128 | data: 'string', 129 | isPadded: true, 130 | }, 131 | { 132 | key: 'pod-ready', 133 | name: 'Ready', 134 | fieldName: 'ready', 135 | minWidth: 70, 136 | maxWidth: 100, 137 | isResizable: true, 138 | onColumnClick: (ev: React.MouseEvent, column: IColumn) : void => { columnClickHandler.current(ev, column); }, 139 | data: 'number', 140 | isPadded: false, 141 | headerClassName : 'DetailsListColumnRight', 142 | onRender: (pod: IPod) => { 143 | var count = 0; 144 | for( var _ in pod.containers ){ 145 | count++; 146 | } 147 | var text = pod.ready + ' / ' + count; 148 | return {text}; 149 | }, 150 | }, 151 | { 152 | key: 'pod-age', 153 | name: 'Age', 154 | fieldName: 'age', 155 | minWidth: 70, 156 | maxWidth: 100, 157 | isResizable: true, 158 | isCollapsible: true, 159 | isPadded: false, 160 | data: 'number', 161 | onColumnClick: (ev: React.MouseEvent, column: IColumn) : void => { columnClickHandler.current(ev, column); }, 162 | headerClassName : 'DetailsListColumnRight', 163 | onRender: (pod: IPod) => { 164 | var age = ""; 165 | if( pod.age > 86400 ) 166 | age = Math.floor(pod.age / 86400).toString() + ' day'; 167 | else if( pod.age > 3600 ) 168 | age = Math.floor(pod.age / 3600).toString() + ' hour'; 169 | else if( pod.age > 60 ) 170 | age = Math.floor(pod.age / 60).toString() + ' min'; 171 | else 172 | age = pod.age.toString() + ' sec'; 173 | return {age}; 174 | }, 175 | }, 176 | { 177 | key: 'pod-cpu', 178 | name: 'CPU', 179 | fieldName: 'cpuPercentage', 180 | minWidth: 250, 181 | maxWidth: 450, 182 | isResizable: true, 183 | isCollapsible: true, 184 | data: 'number', 185 | headerClassName : 'DetailsListColumnRight', 186 | onColumnClick: (ev: React.MouseEvent, column: IColumn) : void => { columnClickHandler.current(ev, column); }, 187 | onRender: (pod: IPod) => { 188 | let cpuUsage = (pod.cpuUsage >= 0) ? (pod.cpuUsage/1000.0).toFixed(1) : '?'; 189 | let cpuLimit = (pod.cpuLimit > 0) ? (pod.cpuLimit/1000.0).toFixed(1) : '∞'; 190 | let text = cpuUsage + ' / ' + cpuLimit + ' core'; 191 | return
192 | }, 193 | isPadded: false, 194 | }, 195 | { 196 | key: 'pod-ram', 197 | name: 'Memory', 198 | fieldName: 'ramPercentage', 199 | minWidth: 250, 200 | maxWidth: 450, 201 | isResizable: true, 202 | isCollapsible: true, 203 | data: 'number', 204 | headerClassName : 'DetailsListColumnRight', 205 | onColumnClick: (ev: React.MouseEvent, column: IColumn) : void => { columnClickHandler.current(ev, column); }, 206 | onRender: (pod: IPod) => { 207 | var text : string; 208 | if( pod.ramLimit >= 1024 * 1024 ) { 209 | let ramUsage = (pod.ramUsage >= 0) ? (pod.ramUsage/1048576.0).toFixed(1) : '?'; 210 | text = ramUsage + ' / ' + (pod.ramLimit/1048576.0).toFixed(1) + ' GB' 211 | } else if( pod.ramLimit >= 1024 ) { 212 | let ramUsage = (pod.ramUsage >= 0) ? (pod.ramUsage/1024.0).toFixed(1) : '?'; 213 | text = ramUsage + ' / ' + (pod.ramLimit/1024.0).toFixed(1) + ' MB' 214 | } else if( pod.ramLimit > 0) { 215 | let ramUsage = (pod.ramUsage >= 0) ? pod.ramUsage : '?'; 216 | text = ramUsage + ' / ' + pod.ramLimit + ' KB' 217 | } else { 218 | if( pod.ramUsage >= 1024 * 1024 ) { 219 | text = (pod.ramUsage/1048576.0).toFixed(1) + ' / ∞ GB' 220 | } else if( pod.ramUsage >= 1024 ) { 221 | text = (pod.ramUsage/1024.0).toFixed(1) + ' / ∞ MB' 222 | } else if( pod.ramUsage >= 0 ) { 223 | text = pod.ramUsage + ' / ∞ KB' 224 | } else { 225 | text = '? / ∞ KB' 226 | } 227 | } 228 | return
229 | }, 230 | isPadded: false, 231 | }, 232 | { 233 | key: 'pod-ip', 234 | name: 'Pod IP', 235 | fieldName: 'podIp', 236 | minWidth: 70, 237 | maxWidth: 290, 238 | isResizable: true, 239 | isCollapsible: true, 240 | data: 'string', 241 | headerClassName : 'DetailsListColumnRight', 242 | onColumnClick: (ev: React.MouseEvent, column: IColumn) : void => { columnClickHandler.current(ev, column); }, 243 | onRender: (pod: IPod) => { 244 | return {pod.podIp}; 245 | }, 246 | }, 247 | { 248 | key: 'host-ip', 249 | name: 'Host IP', 250 | fieldName: 'hostIp', 251 | minWidth: 70, 252 | maxWidth: 290, 253 | isResizable: true, 254 | isCollapsible: true, 255 | data: 'string', 256 | headerClassName : 'DetailsListColumnRight', 257 | onColumnClick: (ev: React.MouseEvent, column: IColumn) : void => { columnClickHandler.current(ev, column); }, 258 | onRender: (pod: IPod) => { 259 | return {pod.hostIp}; 260 | }, 261 | }, 262 | ]; 263 | }, []); 264 | 265 | const reloadPods = useCallback( () => { 266 | var url = (process.env.REACT_APP_BASE_URL || '').trim() + "/api/pods?"; 267 | url += new URLSearchParams({ token : k8sToken, namespace : k8sNamespace }).toString(); 268 | 269 | const fetchData = async () => { 270 | try { 271 | const response = await fetch(url, {mode:'cors'}); 272 | const json = await response.json(); 273 | if(Array.isArray(json)) { // normal response 274 | setPods( 275 | copyAndSort( json, sortedColumn.current, sortedDescending.current ) 276 | ); 277 | setErrorMessage(undefined); 278 | } else { 279 | if( json.error ) 280 | setErrorMessage(json.error); 281 | } 282 | } catch (error) { 283 | setErrorMessage('GET ' + url + ' failed. ' + error); 284 | } 285 | clearTimeout(timer); 286 | timer = setTimeout(reloadPods, 5000); 287 | }; 288 | fetchData(); 289 | }, [k8sToken, k8sNamespace, clearTimeout, setTimeout]); 290 | 291 | 292 | useEffect(() => { 293 | reloadPods(); 294 | }, [reloadPods]); 295 | 296 | 297 | 298 | /* eslint-disable no-unused-vars */ 299 | const onItemInvoked = useCallback( 300 | (pod?: IPod): void => { 301 | if( pod != null){ 302 | let options : IDropdownOption[] = []; 303 | for( var key in pod.containers ){ 304 | options.push({ key : key, text : key}); 305 | } 306 | if( options.length > 0 ){ 307 | setSelectedContainer(options[0].key.toString()); 308 | setDefaultContainer(options[0].key.toString()); 309 | } 310 | setDropDownOptions(options); 311 | setSelectedPod(pod); 312 | openPanel(); 313 | } 314 | }, 315 | [openPanel], 316 | ); 317 | 318 | const onRenderPanelHeader: IRenderFunction = React.useCallback( 319 | (props, defaultRender) => ( 320 | 327 | 328 | {selectedPod?.name} 329 | 330 | 331 | 332 | 333 | 334 | { 335 | setSelectedContainer(option?.key?.toString() || ""); 336 | }} /> 337 | 338 | 339 | ), 340 | [selectedPod, dropDownOptions, defaultContainer], 341 | ); 342 | 343 | 344 | 345 | return ( 346 | <> 347 | { errorMessage && {errorMessage} } 348 | 359 | 369 | 370 | 371 | 372 | ); 373 | }; 374 | 375 | const horizontalGapStackTokens: IStackTokens = { 376 | childrenGap: 10, 377 | }; 378 | 379 | export default PodList; 380 | -------------------------------------------------------------------------------- /ui-app/src/FileExplorer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useCallback, useState, useContext } from 'react'; 2 | import { useBoolean } from '@fluentui/react-hooks'; 3 | import { MessageBar, MessageBarType, TooltipHost, IRenderFunction } from '@fluentui/react'; 4 | import { Separator } from '@fluentui/react/lib/Separator'; 5 | import { TextField } from '@fluentui/react/lib/TextField'; 6 | import { Text } from '@fluentui/react/lib/Text'; 7 | import { ActionButton, IconButton } from '@fluentui/react/lib/Button'; 8 | import { Stack, IStackTokens } from '@fluentui/react'; 9 | import { DetailsListLayoutMode, SelectionMode, IColumn, IDetailsHeaderProps, IDetailsColumnRenderTooltipProps } from '@fluentui/react/lib/DetailsList'; 10 | import { ShimmeredDetailsList } from '@fluentui/react/lib/ShimmeredDetailsList'; 11 | import { mergeStyleSets } from '@fluentui/react/lib/Styling'; 12 | import { Icon } from '@fluentui/react/lib/Icon'; 13 | import { FontIcon } from '@fluentui/react/lib/Icon'; 14 | import { Panel, PanelType, IPanelProps } from '@fluentui/react/lib/Panel'; 15 | import { Link } from '@fluentui/react/lib/Link'; 16 | import { getFileTypeIconProps } from '@fluentui/react-file-type-icons'; 17 | import FileViewer from './FileViewer' 18 | import { K8sToken, K8sNamespace } from './utils' 19 | import IPod, { IPodFile } from './dto' 20 | 21 | 22 | const classNames = mergeStyleSets({ 23 | fileIconHeaderIcon: { 24 | padding: 0, 25 | fontSize: '16px', 26 | }, 27 | fileIconCell: { 28 | textAlign: 'center', 29 | selectors: { 30 | '&:before': { 31 | content: '.', 32 | display: 'inline-block', 33 | verticalAlign: 'middle', 34 | height: '100%', 35 | width: '0px', 36 | visibility: 'hidden', 37 | }, 38 | }, 39 | }, 40 | fileIconImg: { 41 | verticalAlign: 'middle', 42 | maxHeight: '16px', 43 | maxWidth: '16px', 44 | }, 45 | controlWrapper: { 46 | display: 'flex', 47 | flexWrap: 'wrap', 48 | }, 49 | selectionDetails: { 50 | marginBottom: '20px', 51 | }, 52 | grid: { 53 | overflowX: 'hidden', 54 | width: '100%', 55 | '& [role=grid]': { 56 | display: 'flex', 57 | flexDirection: 'column', 58 | alignItems: 'start', 59 | height: 'calc(100vh - 200px)' 60 | }, 61 | '.ms-DetailsList-headerWrapper' : { 62 | flex: '0 0 auto' 63 | }, 64 | '.ms-DetailsList-contentWrapper' : { 65 | width: '100%', 66 | flex: '1 1 auto', 67 | overflowY: 'auto', 68 | overflowX: 'hidden' 69 | }, 70 | '.ms-DetailsRow-cell' : { 71 | display: 'flex', 72 | fontSize: '14px', 73 | alignItems: "center", 74 | } 75 | } 76 | }); 77 | 78 | 79 | const onRenderDetailsHeader: IRenderFunction = (props, defaultRender) => { 80 | if (!props) { 81 | return null; 82 | } 83 | const onRenderColumnHeaderTooltip: IRenderFunction = ( 84 | tooltipHostProps 85 | ) => ; 86 | return defaultRender!({ 87 | ...props, 88 | onRenderColumnHeaderTooltip 89 | }); 90 | }; 91 | 92 | // sort function 93 | type FunctionType = (items: T[], columnKey: string, isSortedDescending?: boolean) => T[]; 94 | const copyAndSort : FunctionType = (items, columnKey, isSortedDescending ) => { 95 | const key = columnKey as keyof IPodFile; 96 | return items.slice(0).sort((a: IPodFile, b: IPodFile) => { 97 | if(a.isDir && !b.isDir ) return -1; 98 | if(!a.isDir && b.isDir ) return 1; 99 | return (isSortedDescending ? (a[key] || 0) < (b[key] || 0) : (a[key] || 0) > (b[key] || 0)) ? 1 : -1; 100 | }); 101 | }; 102 | 103 | type ColumnClickHandler = (ev: React.MouseEvent, column: IColumn) => void; 104 | 105 | 106 | 107 | 108 | 109 | 110 | export interface IFileExplorerComponentProps { 111 | pod : IPod, 112 | containerName : string, 113 | } 114 | 115 | 116 | const FileExplorer: React.FunctionComponent = ({ pod, containerName }) => { 117 | 118 | const k8sToken = useContext(K8sToken); 119 | const k8sNamespace = useContext(K8sNamespace); 120 | const pathRef = useRef("/"); 121 | 122 | const [errorMessage, setErrorMessage] = useState(undefined); 123 | const [isLoading, setLoading] = useState(false); 124 | const [files, setFiles] = useState([]); 125 | const columnsHolder = useRef([]); 126 | const columnClickHandler = useRef((ev: React.MouseEvent, column: IColumn) : void => {}); 127 | 128 | const [selectedFile, setSelectedFile] = useState(undefined); 129 | const [isPanelOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false); 130 | 131 | 132 | useEffect(() => { 133 | columnClickHandler.current = (ev: React.MouseEvent, column: IColumn) : void => { 134 | 135 | const currColumn: IColumn = columnsHolder.current.filter( 136 | (currCol) => column.key === currCol.key 137 | )[0]; 138 | columnsHolder.current.forEach((col: IColumn) => { 139 | if (col === currColumn) { 140 | col.isSortedDescending = !col.isSortedDescending; 141 | col.isSorted = true; 142 | } else { 143 | col.isSorted = false; 144 | col.isSortedDescending = true; 145 | } 146 | }); 147 | 148 | if (files != null) { 149 | setFiles( 150 | copyAndSort( 151 | files, 152 | currColumn.fieldName!, 153 | currColumn.isSortedDescending 154 | ) 155 | ); 156 | } 157 | 158 | }; 159 | }, [files]); 160 | 161 | // download file 162 | const downloadFile = useCallback( (file:IPodFile) => { 163 | let currentPodName = pod.name; 164 | let currencyContainerName = containerName; 165 | var url = (process.env.REACT_APP_BASE_URL || '').trim() + "/api/pod/" + currentPodName + '/' + currencyContainerName + '/file/download?'; 166 | url += new URLSearchParams({ 167 | token : k8sToken, 168 | namespace : k8sNamespace, 169 | path : file.path, 170 | }).toString(); 171 | window.open(url, "_blank"); 172 | }, [containerName, pod, k8sToken, k8sNamespace]); 173 | 174 | // view file 175 | const viewFile = useCallback( (file:IPodFile) => { 176 | setSelectedFile(file); 177 | openPanel(); 178 | }, [openPanel]); 179 | 180 | // initialize columns 181 | useEffect(() => { 182 | pathRef.current = "/"; 183 | setFiles([]); 184 | setLoading(false); 185 | setErrorMessage(""); 186 | columnsHolder.current = [ 187 | { 188 | key: 'file-type', 189 | name: 'File Type', 190 | className: classNames.fileIconCell, 191 | iconClassName: classNames.fileIconHeaderIcon, 192 | iconName: 'Page', 193 | isIconOnly: true, 194 | fieldName: 'name', 195 | minWidth: 16, 196 | maxWidth: 16, 197 | onColumnClick: (ev: React.MouseEvent, column: IColumn) : void => { columnClickHandler.current(ev, column); }, 198 | onRender: (file: IPodFile) => { 199 | if( !file.isDir ){ 200 | let index = file.name.lastIndexOf('.'); 201 | let extension = index > 0 ? file.name.substr(index+1) : file.name; 202 | return 203 | } else { 204 | return 205 | } 206 | 207 | }, 208 | }, 209 | { 210 | key: 'file-name', 211 | name: 'File Name', 212 | fieldName: 'name', 213 | minWidth: 210, 214 | maxWidth: 2000, 215 | isRowHeader: true, 216 | isResizable: true, 217 | isSorted: true, 218 | isSortedDescending: false, 219 | sortAscendingAriaLabel: 'Sorted A to Z', 220 | sortDescendingAriaLabel: 'Sorted Z to A', 221 | onColumnClick: (ev: React.MouseEvent, column: IColumn) : void => { columnClickHandler.current(ev, column); }, 222 | data: 'string', 223 | isPadded: true, 224 | onRender: (file: IPodFile) => { 225 | if( file.isDir ){ 226 | return {file.name} 227 | } else { 228 | var func = (function(){ 229 | return function() { 230 | arguments[0].preventDefault(); 231 | onItemInvoked(file); 232 | }; 233 | })(); 234 | let url = '/#/' + k8sNamespace + '/' + pod.name + '/' + containerName + file.path; 235 | return {file.name} 236 | } 237 | }, 238 | }, 239 | { 240 | key: 'time', 241 | name: 'Time', 242 | fieldName: 'timestamp', 243 | minWidth: 170, 244 | maxWidth: 200, 245 | isResizable: true, 246 | onColumnClick: (ev: React.MouseEvent, column: IColumn) : void => { columnClickHandler.current(ev, column); }, 247 | data: 'string', 248 | isPadded: true, 249 | onRender: (file: IPodFile) => { 250 | return {file.time}; 251 | }, 252 | }, 253 | { 254 | key: 'file-size', 255 | name: 'Size', 256 | fieldName: 'size', 257 | minWidth: 150, 258 | maxWidth: 200, 259 | isResizable: true, 260 | isCollapsible: true, 261 | data: 'number', 262 | onColumnClick: (ev: React.MouseEvent, column: IColumn) : void => { columnClickHandler.current(ev, column); }, 263 | isPadded: false, 264 | onRender: (file: IPodFile) => { 265 | let size = ''; 266 | if( file.size != null){ 267 | if( file.size >= 1073741824 ) { 268 | size = (Math.floor(file.size / 1073741824 * 10) / 10).toString() + ' GB'; 269 | } else if( file.size >= 1048576 ) { 270 | size = (Math.floor(file.size / 1048576 * 10) / 10).toString() + ' MB'; 271 | } else if( file.size >= 1024 ) { 272 | size = (Math.floor(file.size / 1024)).toString() + ' KB'; 273 | } else { 274 | size = file.size + ' B'; 275 | } 276 | } 277 | return {size}; 278 | }, 279 | }, 280 | { 281 | key: 'file-action', 282 | name: 'Action', 283 | fieldName: 'name', 284 | minWidth: 100, 285 | maxWidth: 100, 286 | isResizable: false, 287 | isCollapsible: false, 288 | data: 'string', 289 | isPadded: false, 290 | onRender: (file: IPodFile) => { 291 | var func1 = (function(){ 292 | return function() { 293 | downloadFile(file); 294 | }; 295 | })(); 296 | var func2 = (function(){ 297 | return function() { 298 | viewFile(file); 299 | }; 300 | })(); 301 | return <> 302 | 303 | 304 | 305 | }, 306 | }, 307 | ]; 308 | }, [pod, containerName, downloadFile, viewFile]); 309 | 310 | 311 | const loadFileList = useCallback( () => { 312 | setLoading(true); 313 | let currentPodName = pod.name; 314 | let currencyContainerName = containerName; 315 | let currentPath = pathRef.current; 316 | var url = (process.env.REACT_APP_BASE_URL || '').trim() + "/api/pod/" + currentPodName + '/' + currencyContainerName + '/file/list?'; 317 | url += new URLSearchParams({ 318 | token : k8sToken, 319 | namespace : k8sNamespace, 320 | path : currentPath, 321 | }).toString(); 322 | 323 | 324 | 325 | const fetchData = async () => { 326 | try { 327 | const response = await fetch(url, {mode:'cors'}); 328 | const json = await response.json(); 329 | if(currentPodName !== pod.name || currencyContainerName !== containerName || currentPath !== pathRef.current){ 330 | return; // response is not for the current UI 331 | } 332 | 333 | setLoading(false); 334 | if(Array.isArray(json)) { 335 | setFiles( 336 | copyAndSort( json, "name", false ) 337 | ); 338 | setErrorMessage(undefined); 339 | } else { 340 | if( json.error ) 341 | setErrorMessage(json.error); 342 | } 343 | } catch (error) { 344 | setFiles([]); 345 | setErrorMessage('GET ' + url + ' failed. ' + error); 346 | setLoading(false); 347 | } 348 | }; 349 | fetchData(); 350 | }, [pod, containerName, k8sToken, k8sNamespace]); 351 | 352 | useEffect( () => { 353 | loadFileList() 354 | }, [loadFileList]); 355 | 356 | const onItemInvoked = useCallback( 357 | (file?: IPodFile): void => { 358 | if(file && !isLoading){ 359 | if(file.isDir){ 360 | pathRef.current = file.path; 361 | 362 | 363 | loadFileList(); 364 | } else { // this is a file 365 | let index = file.name.lastIndexOf('.'); 366 | let extension = index > 0 ? file.name.substr(index+1) : file.name; 367 | if( extension === "txt" || extension === "log" || extension === "json" || extension === "yml" || extension === "yaml" || extension === "ini" || extension === "js" || extension === "sh" || extension === "css" || extension === "csv" ){ 368 | viewFile(file); 369 | } else { 370 | downloadFile(file); 371 | } 372 | } 373 | } 374 | }, 375 | [loadFileList, viewFile, downloadFile, isLoading], 376 | ); 377 | 378 | 379 | // go to parent folder 380 | const onBtnParentClicked = useCallback( () => { 381 | if(!isLoading) { 382 | let path = pathRef.current; 383 | path = path.replace(/\/$/, ''); 384 | let index = path.lastIndexOf('/'); 385 | if(index >= 0) 386 | pathRef.current = path.substr(0, index) + '/'; 387 | loadFileList(); 388 | } 389 | 390 | }, [loadFileList, isLoading]); 391 | 392 | const onRenderPanelHeader: IRenderFunction = React.useCallback( 393 | (props, defaultRender) => ( 394 | 401 | 402 | {pod?.name} 403 | 404 | 405 | 406 | 407 | 408 | {containerName} 409 | 410 | 411 | 412 | 413 | 414 | {selectedFile?.path} 415 | 416 | 417 | ), 418 | [pod, containerName, selectedFile], 419 | ); 420 | 421 | 422 | return ( 423 | <> 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | Parent Folder 433 | 434 | 435 | 436 | 437 | { errorMessage && {errorMessage} } 438 | false} 450 | /> 451 | 452 | 462 | 463 | 464 | 465 | ); 466 | }; 467 | 468 | const horizontalGapStackTokens: IStackTokens = { 469 | childrenGap: 10, 470 | }; 471 | 472 | 473 | export default FileExplorer; 474 | -------------------------------------------------------------------------------- /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.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= 26 | github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= 27 | github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= 28 | github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= 29 | github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= 30 | github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= 31 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 32 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 33 | github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= 34 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 35 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 36 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= 37 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 38 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 39 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 40 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 41 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 42 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 43 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 44 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 45 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 46 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 47 | github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= 48 | github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 49 | github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 50 | github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 51 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 52 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 53 | github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 54 | github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= 55 | github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= 56 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 57 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 58 | github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA= 59 | github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk= 60 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 61 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 62 | github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19 h1:J2LPEOcQmWaooBnBtUDV9KHFEnP5LYTZY03GiQ0oQBw= 63 | github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19/go.mod h1:iqneQ2Df3omzIVTkIfn7c1acsVnMGiSLn4XF5Blh3Yg= 64 | github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= 65 | github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM= 66 | github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= 67 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 68 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 69 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 70 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 71 | github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= 72 | github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= 73 | github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= 74 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 75 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 76 | github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= 77 | github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= 78 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 79 | github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 80 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 81 | github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= 82 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 83 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 84 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 85 | github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= 86 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 87 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 88 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 89 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 90 | github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A= 91 | github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= 92 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 93 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 94 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 95 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 96 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 97 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 98 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 99 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 100 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 101 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 102 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 103 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 104 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 105 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 106 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 107 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 108 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 109 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 110 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 111 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 112 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 113 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 114 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 115 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 116 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 117 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 118 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 119 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 120 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 121 | github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= 122 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 123 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 124 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 125 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 126 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 127 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 128 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 129 | github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= 130 | github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 131 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 132 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 133 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 134 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 135 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 136 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 137 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 138 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 139 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 140 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 141 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 142 | github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= 143 | github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= 144 | github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= 145 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 146 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 147 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 148 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 149 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 150 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 151 | github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= 152 | github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 153 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 154 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 155 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 156 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 157 | github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= 158 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 159 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 160 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 161 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 162 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 163 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 164 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 165 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 166 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 167 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 168 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 169 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 170 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 171 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 172 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 173 | github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= 174 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 175 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 176 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 177 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 178 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 179 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 180 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 181 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 182 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 183 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 184 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 185 | github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= 186 | github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= 187 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 188 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 189 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 190 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 191 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 192 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 193 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 194 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 195 | github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 196 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 197 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 198 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 199 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 200 | github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 201 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 202 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 203 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 204 | github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 205 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 206 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 207 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 208 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 209 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 210 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 211 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 212 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 213 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 214 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 215 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 216 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 217 | github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 218 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 219 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 220 | github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= 221 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 222 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 223 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 224 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 225 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 226 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 227 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 228 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 229 | github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E= 230 | github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= 231 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 232 | github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ= 233 | github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= 234 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 235 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 236 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 237 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 238 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 239 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 240 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 241 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 242 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 243 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 244 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 245 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 246 | golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 247 | golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 248 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 249 | golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= 250 | golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 251 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 252 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 253 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 254 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 255 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 256 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 257 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 258 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 259 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 260 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 261 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 262 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 263 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 264 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 265 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 266 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 267 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 268 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 269 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 270 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 271 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 272 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 273 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 274 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 275 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 276 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 277 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 278 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 279 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 280 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 281 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 282 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 283 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 284 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 285 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 286 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 287 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 288 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 289 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 290 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 291 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 292 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 293 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 294 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 295 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 296 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 297 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 298 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 299 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 300 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 301 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 302 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 303 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 304 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 305 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 306 | golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 h1:ADo5wSpq2gqaCGQWzk7S5vd//0iyyLeAratkEoG5dLE= 307 | golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 308 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 309 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 310 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 311 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 312 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= 313 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 314 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 315 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 316 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 317 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 318 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 319 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 320 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 321 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 322 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 323 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 324 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 325 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 326 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 327 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 328 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 329 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 330 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 331 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 332 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 333 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 334 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 335 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 336 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 337 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 338 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 339 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 340 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 341 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 342 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 343 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 344 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 345 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 346 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 347 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 348 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 349 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 350 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 351 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 352 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 353 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 354 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 355 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= 356 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 357 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 358 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 359 | golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 h1:xrCZDmdtoloIiooiA9q0OQb9r8HejIHYoHGhGCe1pGg= 360 | golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 361 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 362 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 363 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= 364 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 365 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 366 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 367 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 368 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 369 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 370 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 371 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 372 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 373 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 374 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 375 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 376 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 377 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= 378 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 379 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 380 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 381 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 382 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 383 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 384 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 385 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 386 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 387 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 388 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 389 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 390 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 391 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 392 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 393 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 394 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 395 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 396 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 397 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 398 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 399 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 400 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 401 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 402 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 403 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 404 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 405 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 406 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 407 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 408 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 409 | golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 410 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 411 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 412 | golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 413 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 414 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 415 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 416 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 417 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 418 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 419 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 420 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 421 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 422 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 423 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 424 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 425 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 426 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 427 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 428 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 429 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 430 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 431 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 432 | google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= 433 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 434 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 435 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 436 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 437 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 438 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 439 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 440 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 441 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 442 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 443 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 444 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 445 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 446 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 447 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 448 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 449 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 450 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 451 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 452 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 453 | google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 454 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 455 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 456 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 457 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 458 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 459 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 460 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 461 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 462 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 463 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 464 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 465 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 466 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 467 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 468 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 469 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 470 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 471 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 472 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 473 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 474 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 475 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 476 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 477 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 478 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 479 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 480 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 481 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 482 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 483 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 484 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 485 | gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 486 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 487 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 488 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 489 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 490 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 491 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 492 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 493 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 494 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 495 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 496 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 497 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 498 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 499 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 500 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 501 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 502 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 503 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 504 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 505 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 506 | k8s.io/api v0.22.1 h1:ISu3tD/jRhYfSW8jI/Q1e+lRxkR7w9UwQEZ7FgslrwY= 507 | k8s.io/api v0.22.1/go.mod h1:bh13rkTp3F1XEaLGykbyRD2QaTTzPm0e/BMd8ptFONY= 508 | k8s.io/api v0.22.2 h1:M8ZzAD0V6725Fjg53fKeTJxGsJvRbk4TEm/fexHMtfw= 509 | k8s.io/api v0.22.2/go.mod h1:y3ydYpLJAaDI+BbSe2xmGcqxiWHmWjkEeIbiwHvnPR8= 510 | k8s.io/apimachinery v0.22.1 h1:DTARnyzmdHMz7bFWFDDm22AM4pLWTQECMpRTFu2d2OM= 511 | k8s.io/apimachinery v0.22.1/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= 512 | k8s.io/apimachinery v0.22.2 h1:ejz6y/zNma8clPVfNDLnPbleBo6MpoFy/HBiBqCouVk= 513 | k8s.io/apimachinery v0.22.2/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= 514 | k8s.io/client-go v0.22.1 h1:jW0ZSHi8wW260FvcXHkIa0NLxFBQszTlhiAVsU5mopw= 515 | k8s.io/client-go v0.22.1/go.mod h1:BquC5A4UOo4qVDUtoc04/+Nxp1MeHcVc1HJm1KmG8kk= 516 | k8s.io/client-go v0.22.2 h1:DaSQgs02aCC1QcwUdkKZWOeaVsQjYvWv8ZazcZ6JcHc= 517 | k8s.io/client-go v0.22.2/go.mod h1:sAlhrkVDf50ZHx6z4K0S40wISNTarf1r800F+RlCF6U= 518 | k8s.io/code-generator v0.22.2/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o= 519 | k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= 520 | k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= 521 | k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= 522 | k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= 523 | k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= 524 | k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= 525 | k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= 526 | k8s.io/metrics v0.22.2 h1:ZQbsg2ENzp+JyhQMp3tsFZK9i5KxvSTDrdkgoWRL568= 527 | k8s.io/metrics v0.22.2/go.mod h1:GUcsBtpsqQD1tKFS/2wCKu4ZBowwRncLOJH1rgWs3uw= 528 | k8s.io/utils v0.0.0-20210707171843-4b05e18ac7d9 h1:imL9YgXQ9p7xmPzHFm/vVd/cF78jad+n4wK1ABwYtMM= 529 | k8s.io/utils v0.0.0-20210707171843-4b05e18ac7d9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= 530 | k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a h1:8dYfu/Fc9Gz2rNJKB9IQRGgQOh2clmRzNIPPY1xLY5g= 531 | k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= 532 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 533 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 534 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 535 | sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= 536 | sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno= 537 | sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= 538 | sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= 539 | sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= 540 | --------------------------------------------------------------------------------