├── .github └── workflows │ └── main.yml ├── .gitignore ├── .jshint ├── LICENSE ├── Portfall.AppImage.desktop ├── Portfall.desktop ├── Portfall.exe.manifest ├── Portfall.ico ├── Portfall.rc ├── README.md ├── appicon.png ├── demo.gif ├── dmg-spec.json ├── frontend ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── package.json.md5 ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json └── src │ ├── App.js │ ├── App.test.js │ ├── blueicon.png │ ├── components │ └── Console.js │ ├── index.css │ ├── index.js │ ├── serviceWorker.js │ └── whiteicon.png ├── go.mod ├── go.sum ├── linuxdeploy-x86_64.AppImage ├── main.go ├── pkg ├── client │ └── client.go ├── favicon │ ├── favicon.go │ └── favicon_test.go ├── logger │ └── logger.go └── os │ └── os.go ├── project.json ├── snapcraft.yaml └── wails.json /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 6 | jobs: 7 | package: 8 | strategy: 9 | matrix: 10 | go-version: [1.14.x] 11 | platform: [ubuntu-latest, macos-latest, windows-latest] 12 | runs-on: ${{ matrix.platform }} 13 | steps: 14 | - name: Install Go 15 | uses: actions/setup-go@v1 16 | with: 17 | go-version: ${{ matrix.go-version }} 18 | - name: Checkout code 19 | uses: actions/checkout@v2 20 | - name: Get wails 21 | run: go get -u github.com/wailsapp/wails/cmd/wails@v1.0.2 22 | - name: Build package osx 23 | run: | 24 | export PATH=${PATH}:`go env GOPATH`/bin 25 | echo "building on ${{ matrix.platform }}" 26 | mkdir -p ~/.wails 27 | cp wails.json ~/.wails/ 28 | export LOG_LEVEL=debug 29 | export GODEBUG=1 30 | wails build -p 31 | ls 32 | echo "turning the .app into a .dmg" 33 | npm install -g appdmg 34 | appdmg dmg-spec.json Portfall.dmg 35 | if: matrix.platform == 'macos-latest' 36 | - name: Build package linux 37 | run: | 38 | sudo apt update && sudo apt install -y libgtk-3-dev libwebkit2gtk-4.0-dev 39 | export PATH=${PATH}:`go env GOPATH`/bin 40 | echo "building on ${{ matrix.platform }}" 41 | mkdir -p ~/.wails 42 | cp wails.json ~/.wails/ 43 | export LOG_LEVEL=debug 44 | export GODEBUG=1 45 | wails build 46 | # turn into app image 47 | wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage 48 | chmod +x linuxdeploy*.AppImage 49 | ls ./ 50 | ./linuxdeploy*.AppImage --appdir AppDir --executable Portfall --desktop-file=Portfall.AppImage.desktop --icon-file=appicon.png --output appimage 51 | if: matrix.platform == 'ubuntu-latest' 52 | - name: Build package windows 53 | run: | 54 | $GP = (go env GOPATH) 55 | $env:path = "$env:path;$GP\bin" 56 | echo "building on ${{ matrix.platform }}" 57 | New-Item -ItemType directory -Path "$HOME\.wails" -Force 58 | Copy-Item -Path "$PWD\wails.json" -Destination "$HOME\.wails\wails.json" 59 | choco install mingw 60 | wails build -p 61 | if: matrix.platform == 'windows-latest' 62 | - name: upload artifact osx 63 | uses: actions/upload-artifact@v1 64 | with: 65 | name: portfall-osx 66 | path: Portfall.dmg 67 | if: matrix.platform == 'macos-latest' 68 | - name: upload artifact linux 69 | uses: actions/upload-artifact@v2-preview 70 | with: 71 | name: portfall-linux 72 | path: Portfall*.AppImage 73 | if: matrix.platform == 'ubuntu-latest' 74 | - name: upload artifact windows 75 | uses: actions/upload-artifact@v1 76 | with: 77 | name: portfall-windows 78 | path: Portfall.exe 79 | if: matrix.platform == 'windows-latest' 80 | 81 | release: 82 | runs-on: ubuntu-latest 83 | needs: package 84 | steps: 85 | - name: Create Release 86 | id: create_release 87 | uses: actions/create-release@v1 88 | env: 89 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 90 | with: 91 | tag_name: ${{ github.ref }} 92 | release_name: Release ${{ github.ref }} 93 | draft: false 94 | prerelease: false 95 | - name: Download osx package 96 | uses: actions/download-artifact@v1 97 | with: 98 | name: portfall-osx 99 | - name: Upload OSX package to release 100 | id: upload-osx-release-asset 101 | uses: actions/upload-release-asset@v1 102 | env: 103 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 104 | with: 105 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 106 | asset_path: ./portfall-osx/Portfall.dmg 107 | asset_name: Portfall.dmg 108 | asset_content_type: application/octet-stream 109 | - name: Download linux package 110 | uses: actions/download-artifact@v1 111 | with: 112 | name: portfall-linux 113 | - id: getfilename 114 | run: echo "::set-output name=file::$(ls portfall-linux/Portfall*.AppImage)" 115 | - name: Upload Linux package to release 116 | id: upload-linux-release-asset 117 | uses: actions/upload-release-asset@v1 118 | env: 119 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 120 | with: 121 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 122 | asset_path: ${{ steps.getfilename.outputs.file }} 123 | asset_name: Portfall-x86_64.AppImage 124 | asset_content_type: application/octet-stream 125 | - name: Download windows package 126 | uses: actions/download-artifact@v1 127 | with: 128 | name: portfall-windows 129 | - name: Upload Windows package to release 130 | id: upload-windows-release-asset 131 | uses: actions/upload-release-asset@v1 132 | env: 133 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 134 | with: 135 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 136 | asset_path: ./portfall-windows/Portfall.exe 137 | asset_name: Portfall.exe 138 | asset_content_type: application/octet-stream 139 | 140 | # snapify: 141 | # runs-on: ubuntu-latest 142 | # steps: 143 | # - name: Check out Git repository 144 | # uses: actions/checkout@v2 145 | # - name: Install Snapcraft 146 | # uses: samuelmeuli/action-snapcraft@v1 147 | # with: 148 | # snapcraft_token: ${{ secrets.snapcraft_token }} 149 | # use_lxd: true 150 | # - name: Build snap 151 | # run: snapcraft --use-lxd 152 | # - name: Release snap 153 | # run: snapcraft push --release=stable portfall_*.snap -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ides 2 | .idea 3 | .vscode 4 | # binary/packaging 5 | portfall 6 | Portfall 7 | porfall.exe 8 | Portfall.exe 9 | portfall-res.syso 10 | Portfall-res.syso 11 | portfall.app 12 | Portfall.app 13 | *.snap 14 | AppDir 15 | Portfall*.AppImage -------------------------------------------------------------------------------- /.jshint: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Reuben Thomas-Davis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Portfall.AppImage.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Portfall 3 | Exec=Portfall 4 | Icon=appicon 5 | Type=Application 6 | Categories=GTK;GNOME;Utility;Development; -------------------------------------------------------------------------------- /Portfall.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Portfall 3 | Exec=portfall 4 | Icon=meta/gui/appicon.png 5 | Type=Application 6 | Categories=GTK;GNOME;Utility;Development; -------------------------------------------------------------------------------- /Portfall.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | true/pm 8 | permonitorv2,permonitor 9 | true 10 | 11 | 12 | -------------------------------------------------------------------------------- /Portfall.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rested/portfall/2915053873191bcb76fbdc24ea235d96a075cd8a/Portfall.ico -------------------------------------------------------------------------------- /Portfall.rc: -------------------------------------------------------------------------------- 1 | 100 ICON "portfall.ico" 2 | 100 24 "portfall.exe.manifest" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Portfall 2 | 3 |

4 | A desktop k8s port-forwarding portal for easy access to all your cluster UIs 5 |

6 | 7 | CodeFactor 8 | 9 | 10 | Contributions Welcome 11 | 12 | 13 | 14 |

15 | 16 | ## Demo 17 |

18 | Demo gif 19 |

20 | 21 | ## Installation 22 | 23 | ### [MacOS](https://github.com/rekon-oss/portfall/releases/latest/download/Portfall.dmg), [Windows](https://github.com/rekon-oss/portfall/releases/latest/download/Portfall.exe) 24 | ### Linux 25 | #### Use the [AppImage](https://github.com/rekon-oss/portfall/releases/latest/download/Portfall-x86_64.AppImage) 26 | Recommend installing [appimaged](https://github.com/AppImage/appimaged) to integrate portfall properly with your system. 27 | ```bash 28 | wget "https://github.com/AppImage/appimaged/releases/download/continuous/appimaged-x86_64.AppImage" 29 | chmod a+x appimaged-x86_64.AppImage 30 | ./appimaged-x86_64.AppImage --install 31 | ``` 32 | 33 | #### Build from source 34 | ```bash 35 | git clone https://github.com/rekon-oss/portfall 36 | cd portfall 37 | # see build requirements for wails here https://github.com/wailsapp/wails#installation 38 | # you will also need go and npm available 39 | go get -u github.com/wailsapp/wails/cmd/wails 40 | wails build 41 | ``` 42 | #### Snap coming soon! 43 | Classic confinement requested [here](https://forum.snapcraft.io/t/classic-confinement-request-for-portfall/16520) 44 | 45 | ## Technical details 46 | 47 | Portfall uses **Go** to do all the Kubernetes work and **React** + **Material UI** for the frontend work. 48 | This is glued together as a single binary with native rendering by the fantastic 49 | [Wails](https://github.com/wailsapp/wails) framework. 50 | 51 | You can read more about it in my blog post here: 52 | https://rekon.uk/2020/04/portfall-a-desktop-k8s-port-forwarding-portal-for-easy-access-to-all-your-cluster-uis/ 53 | 54 | ## License 55 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Frekon-oss%2Fportfall.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Frekon-oss%2Fportfall?ref=badge_large) 56 | -------------------------------------------------------------------------------- /appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rested/portfall/2915053873191bcb76fbdc24ea235d96a075cd8a/appicon.png -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rested/portfall/2915053873191bcb76fbdc24ea235d96a075cd8a/demo.gif -------------------------------------------------------------------------------- /dmg-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Portfall installer", 3 | "background-color": "#326DE6", 4 | "icon-size": 80, 5 | "contents": [ 6 | { "x": 192, "y": 344, "type": "file", "path": "Portfall.app" }, 7 | { "x": 448, "y": 344, "type": "link", "path": "/Applications" } 8 | ] 9 | } -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portfall", 3 | "author": "Reuben Thomas-Davis", 4 | "version": "0.4.0", 5 | "private": true, 6 | "dependencies": { 7 | "@material-ui/core": "^4.9.7", 8 | "@material-ui/icons": "^4.9.1", 9 | "@material-ui/lab": "^4.0.0-alpha.46", 10 | "@wailsapp/runtime": "^1.0.0", 11 | "core-js": "^3.1.4", 12 | "react": "^16.8.6", 13 | "react-dom": "^16.8.6", 14 | "react-modal": "3.8.1", 15 | "typeface-roboto": "0.0.75", 16 | "wails-react-scripts": "3.0.1-2" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /frontend/package.json.md5: -------------------------------------------------------------------------------- 1 | 3cf8772419191558d8f40a3070ad9cf2 -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rested/portfall/2915053873191bcb76fbdc24ea235d96a075cd8a/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /frontend/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 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useRef, useState} from 'react'; 2 | import FormControl from "@material-ui/core/FormControl"; 3 | import {makeStyles} from "@material-ui/core/styles"; 4 | import AppBar from "@material-ui/core/AppBar"; 5 | import Toolbar from "@material-ui/core/Toolbar"; 6 | import Typography from "@material-ui/core/Typography"; 7 | import Autocomplete from '@material-ui/lab/Autocomplete'; 8 | import TextField from "@material-ui/core/TextField"; 9 | import Grid from "@material-ui/core/Grid"; 10 | import {BugReport, Close, Folder, Launch, MoodBadTwoTone, Settings} from "@material-ui/icons"; 11 | import Alert from "@material-ui/lab/Alert"; 12 | import {Card, CircularProgress} from "@material-ui/core"; 13 | import Avatar from "@material-ui/core/Avatar"; 14 | import CardHeader from "@material-ui/core/CardHeader"; 15 | import Button from "@material-ui/core/Button"; 16 | import createMuiTheme from "@material-ui/core/styles/createMuiTheme"; 17 | import {ThemeProvider} from "@material-ui/styles"; 18 | import Popper from "@material-ui/core/Popper"; 19 | import CardContent from "@material-ui/core/CardContent"; 20 | import IconButton from "@material-ui/core/IconButton"; 21 | import Select from "@material-ui/core/Select"; 22 | import MenuItem from "@material-ui/core/MenuItem"; 23 | import InputLabel from "@material-ui/core/InputLabel"; 24 | import whiteIcon from './whiteicon.png'; 25 | import blueIcon from './blueicon.png'; 26 | import Console from "./components/Console"; 27 | 28 | const useStyles = makeStyles(theme => ({ 29 | formControl: { 30 | margin: theme.spacing(1), 31 | color: '#fff', 32 | flexGrow: 1, 33 | }, 34 | selectEmpty: { 35 | marginTop: theme.spacing(2), 36 | }, 37 | title: { 38 | flexGrow: 1, 39 | "& small": { 40 | fontSize: 12, 41 | opacity: 0.7, 42 | verticalAlign: "text-top" 43 | } 44 | }, 45 | cardHeaderTitle: { 46 | minWidth: 10 47 | }, 48 | inputRoot: { 49 | color: "white", 50 | "& .MuiOutlinedInput-notchedOutline": { 51 | borderColor: "white" 52 | }, 53 | "&:hover .MuiOutlinedInput-notchedOutline": { 54 | borderColor: "white" 55 | }, 56 | "&.Mui-focused .MuiOutlinedInput-notchedOutline": { 57 | borderColor: "white" 58 | }, 59 | 60 | }, 61 | endAdornment: { 62 | "& .MuiButtonBase-root": { 63 | color: "white" 64 | } 65 | }, 66 | inputLabelRoot: { 67 | color: "white" 68 | } 69 | })); 70 | const theme = createMuiTheme({ 71 | palette: { 72 | primary: {main: "rgb(50,109,230)"}, 73 | secondary: {main: "#ccc"} 74 | }, 75 | }); 76 | 77 | function usePrevious(value) { 78 | const ref = useRef(); 79 | useEffect(() => { 80 | ref.current = value; 81 | }); 82 | return ref.current; 83 | } 84 | 85 | function App() { 86 | const classes = useStyles(); 87 | const [namespaces, setNamespaces] = useState([]); 88 | const [selectedNamespaces, setSelectedNS] = useState([]); 89 | const [anchorEl, setAnchorEl] = useState(null); 90 | const prevSelectedNamespaces = usePrevious(selectedNamespaces); 91 | const [configFilePath, setConfigFilePath] = useState(null) 92 | const confPathEl = useRef(null) 93 | const [loading, setLoading] = useState(true); 94 | const [websites, setWebsites] = useState([]); 95 | const [showConfig, setShowConfig] = useState(false); 96 | const [configMessage, setConfigMessage] = useState(null); 97 | const [availableContexts, setAvailableContexts] = useState([]); 98 | const [currentContext, setCurrentContext] = useState(null); 99 | const [version, setVersion] = useState(null); 100 | const [showConsole, setShowConsole] = useState(false); 101 | // const prevContext = usePrevious(currentContext); 102 | 103 | 104 | console.log() 105 | useEffect(() => { 106 | window.backend.Client.GetCurrentConfigPath().then(cp => { 107 | if (cp) { 108 | setConfigFilePath(cp); 109 | refreshContext(); 110 | } else { 111 | Promise.all([window.backend.Client.GetAvailableContexts(), window.backend.Client.GetCurrentContext()]).then(([acs, cc]) => { 112 | console.log("current context", cc, ", available:", acs); 113 | setAvailableContexts(acs); 114 | setCurrentContext(cc); 115 | }); 116 | } 117 | 118 | }); 119 | window.backend.PortfallOS.GetVersion().then(v => { 120 | setVersion(v); 121 | }) 122 | 123 | 124 | }, []); 125 | 126 | const refreshContext = () => { 127 | setWebsites([]); 128 | setLoading(true); 129 | window.backend.Client.ListNamespaces().then((r) => { 130 | setNamespaces(r); 131 | setSelectedNS(["default"]); 132 | setLoading(false) 133 | }); 134 | Promise.all([window.backend.Client.GetAvailableContexts(), window.backend.Client.GetCurrentContext()]).then(([acs, cc]) => { 135 | console.log("current context", cc, ", available:", acs); 136 | setAvailableContexts(acs); 137 | setCurrentContext(cc); 138 | }); 139 | 140 | } 141 | 142 | useEffect(() => { 143 | if (selectedNamespaces !== null && prevSelectedNamespaces !== selectedNamespaces) { 144 | setLoading(true); 145 | // get ns to add 146 | const nsToAdd = selectedNamespaces.find(ns => !(prevSelectedNamespaces || []).includes(ns)); 147 | const namespacesToRemove = (prevSelectedNamespaces || []).filter(ns => !selectedNamespaces.includes(ns)); 148 | let newWebsites = websites; 149 | if (namespacesToRemove) { 150 | Promise.all(namespacesToRemove.map(ns => window.backend.Client.RemoveWebsitesInNamespace(ns))).then(() => { 151 | console.log("removed namespaces", namespacesToRemove) 152 | }); 153 | newWebsites = websites.filter(w => { 154 | if (namespacesToRemove.includes(w.namespace)) { 155 | return false 156 | } 157 | if (namespacesToRemove.includes("All Namespaces") && !selectedNamespaces.includes(w.namespace)) { 158 | return false 159 | } 160 | return true 161 | }); 162 | } 163 | if (nsToAdd) { 164 | console.log("adding ns", nsToAdd) 165 | window.backend.Client.GetWebsitesInNamespace(nsToAdd).then(results => { 166 | console.log("received websites in ns to add", results) 167 | // when we already have all namespaces there is no need to concat the results of the given website 168 | if (results && !(prevSelectedNamespaces || []).includes("All Namespaces")) { 169 | const resObj = JSON.parse(results) 170 | if (resObj) { 171 | newWebsites = newWebsites.concat(resObj); 172 | } 173 | } 174 | // if we just added All namespaces the portforwards will be reset so we sh 175 | setWebsites(newWebsites); 176 | setLoading(false); 177 | }); 178 | } else { 179 | setWebsites(newWebsites); 180 | setLoading(false) 181 | } 182 | 183 | } 184 | }, [selectedNamespaces, prevSelectedNamespaces, websites]); 185 | 186 | 187 | return ( 188 | 189 | Blue Portfall logo in background 199 |
200 | 201 | 202 | White Portfall logo in toolbar 204 | 205 | Portfall {version ? {version} : null} 206 | 207 | {/* todo: arrange by namespace */} 208 | 209 | { 217 | // 218 | setSelectedNS(value) 219 | }} 220 | renderInput={params => ( 221 | ) 228 | }/> 229 | 230 | 231 | 232 |
239 | 243 | {!configFilePath ? ( 244 | } severity="error"> 245 | No config file found 246 | 247 | ) : null} 248 | {(configFilePath && websites.length === 0 && namespaces.length) ? ( 249 | } severity="info"> 250 | No websites found to port-forward in the selected namespace(s) 251 | 252 | ) : null} 253 | {(configFilePath && !namespaces.length) ? ( 254 | } severity="warning"> 255 | Invalid context, try updating your config or switching context 256 | 257 | ) : null} 258 | {websites.map(({localPort, podPort, title, iconRemoteUrl}) => ( 259 | 260 | 261 | } 263 | title={{title}} 264 | subheader={{localPort}:{podPort}} action={ 265 | }/> 270 | 271 | 272 | 273 | ))} 274 | {loading ? : null} 275 | 276 |
277 | { 280 | setShowConsole(!showConsole); 281 | setAnchorEl(e.currentTarget); 282 | setShowConfig(false); 283 | }} 284 | > 285 | 286 | 287 | { 290 | setAnchorEl(e.currentTarget); 291 | setShowConfig(!showConfig) 292 | setShowConsole(false); 293 | }} 294 | > 295 | 296 | 297 | setShowConsole(false)} disablePortal={false} 299 | modifiers={{ 300 | flip: { 301 | enabled: true, 302 | }, 303 | arrow: { 304 | enabled: false, 305 | // element: arrowRef, 306 | }, 307 | }}> 308 | 309 | 311 | setShowConsole(false)}>}/> 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | setShowConfig(false)} disablePortal={false} 322 | modifiers={{ 323 | flip: { 324 | enabled: true, 325 | }, 326 | arrow: { 327 | enabled: false, 328 | // element: arrowRef, 329 | }, 330 | }}> 331 | 332 | setShowConfig(false)}>}/> 334 | 335 | 336 | 337 | 338 | 340 | 341 | 342 | 350 | 351 | {(availableContexts && currentContext) ? 352 | 353 | 354 | Config context 355 | 359 | 360 | : null} 361 | {configMessage ? ( 362 | 363 | { 364 | setConfigMessage(null) 365 | }}> 366 | {configMessage.message} 367 | 368 | ) : null} 369 | 370 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 |
400 |
401 | ); 402 | } 403 | 404 | export default App; 405 | -------------------------------------------------------------------------------- /frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | // todo: frontend testing 6 | it('renders without crashing', () => { 7 | const div = document.createElement('div'); 8 | ReactDOM.render(, div); 9 | ReactDOM.unmountComponentAtNode(div); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/src/blueicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rested/portfall/2915053873191bcb76fbdc24ea235d96a075cd8a/frontend/src/blueicon.png -------------------------------------------------------------------------------- /frontend/src/components/Console.js: -------------------------------------------------------------------------------- 1 | import React, {useCallback, useEffect, useState} from 'react'; 2 | import * as Wails from "@wailsapp/runtime"; 3 | import {GitHub} from "@material-ui/icons"; 4 | import Button from "@material-ui/core/Button"; 5 | 6 | const colorMap = { 7 | "debug": "cyan", 8 | "info": "grey", 9 | "warn": "yellow", 10 | "error": "red", 11 | "fatal": "darkred" 12 | } 13 | 14 | function Console() { 15 | const [logLines, setLogLines] = useState([]) 16 | 17 | const handleMessage = useCallback(msg => { 18 | setLogLines(prevLines => prevLines.concat([msg])) 19 | }, []) 20 | 21 | useEffect(() => { 22 | 23 | 24 | // react to the wails events 25 | Wails.Events.On("log:debug", msg => { 26 | console.debug(msg); 27 | handleMessage({level: "debug", message: msg}) 28 | 29 | }); 30 | Wails.Events.On("log:info", msg => { 31 | console.info(msg); 32 | handleMessage({level: "info", message: msg}) 33 | 34 | }); 35 | Wails.Events.On("log:warn", msg => { 36 | console.warn(msg); 37 | handleMessage({level: "warn", message: msg}) 38 | 39 | }); 40 | Wails.Events.On("log:error", msg => { 41 | console.error(msg); 42 | handleMessage({level: "error", message: msg}) 43 | 44 | }); 45 | Wails.Events.On("log:fatal", msg => { 46 | console.error(msg); 47 | handleMessage({level: "fatal", message: msg}) 48 | }); 49 | 50 | }, [handleMessage]) 51 | 52 | return 53 | 60 |
61 | {logLines.map(({level, message}) => { 62 | return

[{level.toUpperCase()}]: {message}

64 | })} 65 |
66 |
67 | } 68 | 69 | export default Console; -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | overflow-x: hidden; 4 | } 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import 'core-js/stable'; 4 | import './index.css'; 5 | import 'typeface-roboto'; 6 | import App from './App'; 7 | 8 | import * as Wails from '@wailsapp/runtime'; 9 | 10 | Wails.Init(() => { 11 | ReactDOM.render(, document.getElementById('app')); 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /frontend/src/whiteicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rested/portfall/2915053873191bcb76fbdc24ea235d96a075cd8a/frontend/src/whiteicon.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module portfall 2 | 3 | require ( 4 | github.com/Masterminds/semver v1.5.0 // indirect 5 | github.com/PuerkitoBio/goquery v1.5.1 6 | github.com/fatih/color v1.9.0 // indirect 7 | github.com/leaanthony/mewn v0.10.7 8 | github.com/leaanthony/slicer v1.4.1 // indirect 9 | github.com/mattn/go-colorable v0.1.6 // indirect 10 | github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 11 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 12 | github.com/pkg/errors v0.9.1 // indirect 13 | github.com/wailsapp/wails v1.0.2 14 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120 // indirect 15 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 // indirect 16 | gopkg.in/AlecAivazis/survey.v1 v1.8.8 // indirect 17 | gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86 // indirect 18 | k8s.io/api v0.17.4 19 | k8s.io/apimachinery v0.17.4 20 | k8s.io/client-go v0.17.4 21 | ) 22 | 23 | go 1.13 24 | -------------------------------------------------------------------------------- /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 | github.com/AlecAivazis/survey/v2 v2.0.5/go.mod h1:WYBhg6f0y/fNYUuesWQc0PKbJcEliGcYHB9sNT3Bg74= 5 | github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= 6 | github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= 7 | github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= 8 | github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= 9 | github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= 10 | github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= 11 | github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= 12 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 13 | github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= 14 | github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 15 | github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= 16 | github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 17 | github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= 18 | github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= 19 | github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE= 20 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 21 | github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 22 | github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 23 | github.com/abadojack/whatlanggo v1.0.1 h1:19N6YogDnf71CTHm3Mp2qhYfkRdyvbgwWdd2EPxJRG4= 24 | github.com/abadojack/whatlanggo v1.0.1/go.mod h1:66WiQbSbJBIlOZMsvbKe5m6pzQovxCH9B/K8tQB2uoc= 25 | github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= 26 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 27 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 28 | github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 29 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 30 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 31 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 32 | github.com/dchest/cssmin v0.0.0-20151210170030-fb8d9b44afdc/go.mod h1:ABJPuor7YlcsHmvJ1QxX38e2NcufLY3hm0yXv+cy9sI= 33 | github.com/dchest/htmlmin v0.0.0-20150526090704-e254725e81ac/go.mod h1:XsAE+b4rOZc8gvgsgF+wU75mNBvBcyED1wdd9PBLlJ0= 34 | github.com/dchest/jsmin v0.0.0-20160823214000-faeced883947/go.mod h1:Dv9D0NUlAsaQcGQZa5kc5mqR9ua72SmA8VXi4cd+cBw= 35 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 36 | github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg= 37 | github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= 38 | github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e h1:p1yVGRW3nmb85p1Sh1ZJSDm4A4iKLS5QNbvUHMgGu/M= 39 | github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 40 | github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 41 | github.com/evanphx/json-patch v4.2.0+incompatible h1:fUDGZCv/7iAN7u0puUVhvKCcsR6vRfwrJatElLBEf0I= 42 | github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 43 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 44 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 45 | github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= 46 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 47 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 48 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 49 | github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 50 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 51 | github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= 52 | github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= 53 | github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= 54 | github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= 55 | github.com/go-playground/colors v1.2.0 h1:0EdjTXKrr2g1L/LQTYtIqabeHpZuGZz1U4osS1T8+5M= 56 | github.com/go-playground/colors v1.2.0/go.mod h1:miw1R2JIE19cclPxsXqNdzLZsk4DP4iF+m88bRc7kfM= 57 | github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= 58 | github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 59 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 60 | github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 61 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 62 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 63 | github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 64 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 65 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 66 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 67 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 68 | github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= 69 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 70 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 71 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 72 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 73 | github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= 74 | github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= 75 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 76 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 77 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 78 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 79 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 80 | github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k= 81 | github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 82 | github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= 83 | github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= 84 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 85 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= 86 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 87 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 88 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 89 | github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= 90 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 91 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 92 | github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= 93 | github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 94 | github.com/jackmordaunt/icns v1.0.0 h1:RYSxplerf/l/DUd09AHtITwckkv/mqjVv4DjYdPmAMQ= 95 | github.com/jackmordaunt/icns v1.0.0/go.mod h1:7TTQVEuGzVVfOPPlLNHJIkzA6CoV7aH1Dv9dW351oOo= 96 | github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 97 | github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= 98 | github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 99 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 100 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 101 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 102 | github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= 103 | github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= 104 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 105 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 106 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 107 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= 108 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 109 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 110 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 111 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 112 | github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 113 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 114 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 115 | github.com/leaanthony/mewn v0.10.7 h1:jCcNJyIUOpwj+I5SuATvCugDjHkoo+j6ubEOxxrxmPA= 116 | github.com/leaanthony/mewn v0.10.7/go.mod h1:CRkTx8unLiSSilu/Sd7i1LwrdaAL+3eQ3ses99qGMEQ= 117 | github.com/leaanthony/slicer v1.4.0 h1:Q9u4w+UBU4WHjXnEDdz+eRLMKF/rnyosRBiqULnc1J8= 118 | github.com/leaanthony/slicer v1.4.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= 119 | github.com/leaanthony/slicer v1.4.1 h1:X/SmRIDhkUAolP79mSTO0jTcVX1k504PJBqvV6TwP0w= 120 | github.com/leaanthony/slicer v1.4.1/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= 121 | github.com/leaanthony/spinner v0.5.3 h1:IMTvgdQCec5QA4qRy0wil4XsRP+QcG1OwLWVK/LPZ5Y= 122 | github.com/leaanthony/spinner v0.5.3/go.mod h1:oHlrvWicr++CVV7ALWYi+qHk/XNA91D9IJ48IqmpVUo= 123 | github.com/leaanthony/synx v0.1.0 h1:R0lmg2w6VMb8XcotOwAe5DLyzwjLrskNkwU7LLWsyL8= 124 | github.com/leaanthony/synx v0.1.0/go.mod h1:Iz7eybeeG8bdq640iR+CwYb8p+9EOsgMWghkSRyZcqs= 125 | github.com/leaanthony/wincursor v0.1.0 h1:Dsyp68QcF5cCs65AMBmxoYNEm0n8K7mMchG6a8fYxf8= 126 | github.com/leaanthony/wincursor v0.1.0/go.mod h1:7TVwwrzSH/2Y9gLOGH+VhA+bZhoWXBRgbGNTMk+yimE= 127 | github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 128 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 129 | github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= 130 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 131 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 132 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 133 | github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= 134 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 135 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 136 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 137 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 138 | github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= 139 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 140 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 141 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 142 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 143 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 144 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= 145 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 146 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 147 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 148 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 149 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 150 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 151 | github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 152 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 153 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 154 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 155 | github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 156 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 157 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= 158 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= 159 | github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 160 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 161 | github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= 162 | github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 163 | github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 164 | github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= 165 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 166 | github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= 167 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 168 | github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= 169 | github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= 170 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= 171 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= 172 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 173 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 174 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 175 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 176 | github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 177 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 178 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 179 | github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= 180 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 181 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 182 | github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 183 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 184 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 185 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 186 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 187 | github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 188 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 189 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 190 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 191 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 192 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 193 | github.com/syossan27/tebata v0.0.0-20180602121909-b283fe4bc5ba h1:2DHfQOxcpWdGf5q5IzCUFPNvRX9Icf+09RvQK2VnJq0= 194 | github.com/syossan27/tebata v0.0.0-20180602121909-b283fe4bc5ba/go.mod h1:iLnlXG2Pakcii2CU0cbY07DRCSvpWNa7nFxtevhOChk= 195 | github.com/wailsapp/wails v1.0.2 h1:ztqaLpDb8m8SqAi0u+Ss49zBFrmtm/cAa6ztvKYEjaw= 196 | github.com/wailsapp/wails v1.0.2/go.mod h1:p+1LmkP+8qF/WFancyMPLKw4jukT9BTAPoZzdn6uwx0= 197 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 198 | golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 199 | golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 200 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 201 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 202 | golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 203 | golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947M/+gp0+CqQXDtMRC0fseo= 204 | golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 205 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 206 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 207 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 208 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 209 | golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 210 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 211 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 212 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 213 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 214 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 215 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 216 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 217 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 218 | golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 h1:6M3SDHlHHDCx2PcQw3S4KsR170vGqDhJDOmpVd4Hjak= 219 | golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 220 | golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI= 221 | golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 222 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= 223 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 224 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120 h1:EZ3cVSzKOlJxAd8e8YAJ7no8nNypTxexh/YE/xW3ZEY= 225 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 226 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 227 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 228 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= 229 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 230 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 231 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 232 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 233 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 234 | golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 235 | golang.org/x/sys v0.0.0-20180606202747-9527bec2660b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 236 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 237 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 238 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 239 | golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 240 | golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 241 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 242 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 243 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 244 | golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 h1:rM0ROo5vb9AdYJi1110yjWGMej9ITfKddS89P3Fkhug= 245 | golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 246 | golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 247 | golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg= 248 | golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 249 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 250 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 251 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 252 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 253 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 h1:YTzHMGlqJu67/uEo1lBv0n3wBXhXNeUbB1XfN2vmTm0= 254 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 255 | golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 256 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 257 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 258 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 259 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 260 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 261 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 262 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= 263 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 264 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 265 | golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 266 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 267 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 268 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 269 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 270 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 271 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 272 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 273 | google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= 274 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 275 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 276 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 277 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 278 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 279 | gopkg.in/AlecAivazis/survey.v1 v1.8.4 h1:10xXXN3wgIhPheb5NI58zFgZv32Ana7P3Tl4shW+0Qc= 280 | gopkg.in/AlecAivazis/survey.v1 v1.8.4/go.mod h1:iBNOmqKz/NUbZx3bA+4hAGLRC7fSK7tgtVDT4tB22XA= 281 | gopkg.in/AlecAivazis/survey.v1 v1.8.8 h1:5UtTowJZTz1j7NxVzDGKTz6Lm9IWm8DDF6b7a2wq9VY= 282 | gopkg.in/AlecAivazis/survey.v1 v1.8.8/go.mod h1:CaHjv79TCgAvXMSFJSVgonHXYWxnhzI3eoHtnX5UgUo= 283 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 284 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 285 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 286 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 287 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 288 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 289 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 290 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 291 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 292 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 293 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 294 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 295 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 296 | gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22 h1:0efs3hwEZhFKsCoP8l6dDB1AZWMgnEl3yWXWRZTOaEA= 297 | gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 298 | gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86 h1:OfFoIUYv/me30yv7XlMy4F9RJw8DEm8WQ6QG1Ph4bH0= 299 | gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 300 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 301 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 302 | k8s.io/api v0.17.4 h1:HbwOhDapkguO8lTAE8OX3hdF2qp8GtpC9CW/MQATXXo= 303 | k8s.io/api v0.17.4/go.mod h1:5qxx6vjmwUVG2nHQTKGlLts8Tbok8PzHl4vHtVFuZCA= 304 | k8s.io/apimachinery v0.17.4 h1:UzM+38cPUJnzqSQ+E1PY4YxMHIzQyCg29LOoGfo79Zw= 305 | k8s.io/apimachinery v0.17.4/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g= 306 | k8s.io/client-go v0.17.4 h1:VVdVbpTY70jiNHS1eiFkUt7ZIJX3txd29nDxxXH4en8= 307 | k8s.io/client-go v0.17.4/go.mod h1:ouF6o5pz3is8qU0/qYL2RnoxOPqgfuidYLowytyLJmc= 308 | k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= 309 | k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 310 | k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 311 | k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= 312 | k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= 313 | k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU= 314 | k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= 315 | k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo= 316 | k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= 317 | sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= 318 | sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= 319 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 320 | -------------------------------------------------------------------------------- /linuxdeploy-x86_64.AppImage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rested/portfall/2915053873191bcb76fbdc24ea235d96a075cd8a/linuxdeploy-x86_64.AppImage -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/leaanthony/mewn" 5 | "github.com/wailsapp/wails" 6 | "portfall/pkg/client" 7 | "portfall/pkg/os" 8 | ) 9 | 10 | func main() { 11 | 12 | js := mewn.String("./frontend/build/static/js/main.js") 13 | css := mewn.String("./frontend/build/static/css/main.css") 14 | 15 | c := &client.Client{} 16 | o := &os.PortfallOS{} 17 | 18 | app := wails.CreateApp(&wails.AppConfig{ 19 | Width: 1024, 20 | Height: 768, 21 | Title: "Portfall", 22 | JS: js, 23 | CSS: css, 24 | Colour: "#fff", 25 | Resizable: true, 26 | }) 27 | app.Bind(c) 28 | app.Bind(o) 29 | app.Run() 30 | } 31 | -------------------------------------------------------------------------------- /pkg/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "github.com/phayes/freeport" 8 | "github.com/wailsapp/wails" 9 | v1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/client-go/kubernetes" 12 | "k8s.io/client-go/rest" 13 | "k8s.io/client-go/tools/clientcmd" 14 | "k8s.io/client-go/tools/clientcmd/api" 15 | "k8s.io/client-go/tools/portforward" 16 | "k8s.io/client-go/transport/spdy" 17 | "net/http" 18 | "net/url" 19 | "os" 20 | "path/filepath" 21 | "portfall/pkg/favicon" 22 | "portfall/pkg/logger" 23 | "strings" 24 | "sync" 25 | "time" 26 | ) 27 | 28 | // Client is the core struct of Portfall - references k8s client and config and tracks active websites and namespaces 29 | type Client struct { 30 | s *kubernetes.Clientset 31 | conf *rest.Config 32 | rawConf *api.Config 33 | configPath string 34 | currentContext string 35 | websites []*Website 36 | activeNamespaces []string 37 | log *logger.CustomLogger 38 | } 39 | 40 | // Handles ongoing port-forwards for websites 41 | type portForwardPodRequest struct { 42 | RestConfig *rest.Config 43 | // Pod is the selected pod for this port forwarding 44 | Pod v1.Pod 45 | // LocalPort is the local port that will be selected to expose the PodPort 46 | LocalPort int32 47 | // PodPort is the target port for the pod 48 | PodPort int32 49 | // StopCh is the channel used to manage the port forward lifecycle 50 | StopCh chan struct{} 51 | // ReadyCh communicates when the tunnel is ready to receive traffic 52 | ReadyCh chan struct{} 53 | } 54 | 55 | // Website is the internal representation of a Website 56 | type Website struct { 57 | isForwarded bool 58 | portForwardReq portForwardPodRequest 59 | icon favicon.Icon 60 | // public 61 | LocalPort int32 `json:"localPort"` 62 | PodPort int32 `json:"podPort"` 63 | Title string `json:"title"` 64 | IconUrl string `json:"iconUrl"` 65 | IconRemoteUrl string `json:"iconRemoteUrl"` 66 | Namespace string `json:"namespace"` 67 | PodName string `json:"podName"` 68 | } 69 | 70 | // PortForwardAPdd takes a portForwardPodRequest and creates the port forward to the given pod 71 | // usage based on https://github.com/gianarb/kube-port-forward 72 | func portForwardAPod(req portForwardPodRequest) error { 73 | path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward", 74 | req.Pod.Namespace, req.Pod.Name) 75 | hostIP := strings.TrimLeft(req.RestConfig.Host, "htps:/") 76 | 77 | transport, upgrader, err := spdy.RoundTripperFor(req.RestConfig) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | dialer := spdy.NewDialer( 83 | upgrader, 84 | &http.Client{Transport: transport}, 85 | http.MethodPost, 86 | &url.URL{Scheme: "https", Path: path, Host: hostIP}) 87 | 88 | fw, err := portforward.New( 89 | dialer, 90 | []string{fmt.Sprintf("%d:%d", req.LocalPort, req.PodPort)}, 91 | req.StopCh, 92 | req.ReadyCh, 93 | os.Stdout, 94 | os.Stderr) 95 | 96 | if err != nil { 97 | return err 98 | } 99 | return fw.ForwardPorts() 100 | } 101 | 102 | func (c *Client) getWebsiteForPort(pod v1.Pod, containerPort int32) (*Website, error) { 103 | localPort, err := freeport.GetFreePort() 104 | if err != nil { 105 | return nil, err 106 | } 107 | // stopCh control the port forwarding lifecycle. When it gets closed the 108 | // port forward will terminate 109 | stopCh := make(chan struct{}, 1) 110 | // readyCh communicate when the port forward is ready to get traffic 111 | readyCh := make(chan struct{}) 112 | portForwardReq := portForwardPodRequest{ 113 | RestConfig: c.conf, 114 | Pod: pod, 115 | LocalPort: int32(localPort), 116 | PodPort: containerPort, 117 | StopCh: stopCh, 118 | ReadyCh: readyCh, 119 | } 120 | go func() { 121 | err := portForwardAPod(portForwardReq) 122 | if err != nil { 123 | c.log.Debugf("%v", err) 124 | } 125 | }() 126 | 127 | select { 128 | case <-readyCh: 129 | break 130 | case <-time.After(10 * time.Second): 131 | close(stopCh) 132 | //close(readyCh) 133 | return nil, fmt.Errorf("timed out of portforward for pod %s on port %d after 10 seconds", pod.Name, portForwardReq.PodPort) 134 | } 135 | website := Website{ 136 | isForwarded: true, 137 | LocalPort: int32(localPort), 138 | PodPort: containerPort, 139 | portForwardReq: portForwardReq, 140 | } 141 | 142 | // get the favicon 143 | bestIcon, err := favicon.GetBest(fmt.Sprintf("http://localhost:%d", localPort)) 144 | if err != nil { 145 | close(stopCh) 146 | return nil, err 147 | } 148 | website.icon = *bestIcon 149 | return &website, nil 150 | } 151 | 152 | // ListNamespaces returns a list of the names of available namespaces in the current cluster 153 | func (c *Client) ListNamespaces() (nsList []string) { 154 | namespaces, err := c.s.CoreV1().Namespaces().List(metav1.ListOptions{}) 155 | if err != nil { 156 | c.log.Warnf("Found no namespaces") 157 | c.log.Errorf("%v", err) 158 | return make([]string, 0) 159 | } 160 | for _, ns := range namespaces.Items { 161 | nsList = append(nsList, ns.Name) 162 | } 163 | c.log.Infof("Found the following namespaces %v", nsList) 164 | return nsList 165 | } 166 | 167 | // RemoveWebsitesInNamespace takes a namespace's name and stops port-forwarding all websites and removes them 168 | // from Client.websites and finally removes the namespace from Client.activeNamespaces 169 | func (c *Client) RemoveWebsitesInNamespace(namespace string) { 170 | var newWebsites []*Website 171 | var newNamespaces []string 172 | websiteLoop: 173 | for _, website := range c.websites { 174 | if website.portForwardReq.Pod.Namespace == namespace { 175 | close(website.portForwardReq.StopCh) 176 | continue 177 | } 178 | if namespace == "All Namespaces" { 179 | for _, ns := range c.activeNamespaces { 180 | if website.portForwardReq.Pod.Namespace == ns { 181 | // don't close pods in still active namespaces 182 | continue websiteLoop 183 | } 184 | } 185 | close(website.portForwardReq.StopCh) 186 | continue 187 | } 188 | newWebsites = append(newWebsites, website) 189 | } 190 | for _, ns := range c.activeNamespaces { 191 | if ns != namespace { 192 | newNamespaces = append(newNamespaces, ns) 193 | } 194 | } 195 | c.activeNamespaces = newNamespaces 196 | c.websites = newWebsites 197 | } 198 | 199 | func (c *Client) handleWebsiteAdding(p v1.Pod, tp int32, resourceName string, resourceType string, queue chan *Website) { 200 | ws, err := c.getWebsiteForPort(p, tp) 201 | if err != nil { 202 | c.log.Warnf("Failed to get icons for pod %s in %s %s on port %d", p.Name, resourceName, resourceType, tp) 203 | c.log.Errorf("%v", err) 204 | queue <- &Website{} 205 | } else { 206 | queue <- ws 207 | } 208 | } 209 | 210 | func (c *Client) handleServicesInPod(services *v1.ServiceList, pod v1.Pod, wg *sync.WaitGroup, queue chan *Website) (handledPorts []int32) { 211 | for _, svc := range services.Items { 212 | matchCount := 0 213 | for k, v := range svc.Spec.Selector { 214 | if pod.Labels[k] == v { 215 | matchCount++ 216 | } 217 | } 218 | if matchCount == len(svc.Spec.Selector) { 219 | portIter: 220 | for _, port := range svc.Spec.Ports { 221 | for _, p := range handledPorts { 222 | if p == port.TargetPort.IntVal { 223 | // this port has already been handled by another service so we are safe to skip it 224 | c.log.Infof("skipped port %d for service %s as it has already been handled", p, svc.Name) 225 | 226 | continue portIter 227 | } 228 | } 229 | handledPorts = append(handledPorts, port.TargetPort.IntVal) 230 | wg.Add(1) 231 | go c.handleWebsiteAdding(pod, port.TargetPort.IntVal, svc.Name, "service", queue) 232 | } 233 | } 234 | } 235 | return handledPorts 236 | } 237 | 238 | func (c *Client) handleContainerPortsInPod(pod v1.Pod, handledPorts []int32, wg *sync.WaitGroup, queue chan *Website) { 239 | for _, container := range pod.Spec.Containers { 240 | cpLoop: 241 | for _, port := range container.Ports { 242 | for _, hp := range handledPorts { 243 | if port.ContainerPort == hp { 244 | continue cpLoop 245 | } 246 | } 247 | wg.Add(1) 248 | go c.handleWebsiteAdding(pod, port.ContainerPort, container.Name, "container", queue) 249 | } 250 | } 251 | } 252 | 253 | func (c *Client) forwardAndGetIconsForWebsitesInNamespace(namespace string) ([]*Website, error) { 254 | var nsWebsites []*Website 255 | internalNS := namespace 256 | if namespace == "All Namespaces" { 257 | internalNS = "" 258 | } 259 | pods, err := c.s.CoreV1().Pods(internalNS).List(metav1.ListOptions{}) 260 | if err != nil { 261 | c.log.Warnf("Failed to get pods in ns %s", namespace) 262 | return nil, err 263 | } 264 | services, err := c.s.CoreV1().Services(internalNS).List(metav1.ListOptions{}) 265 | if err != nil { 266 | c.log.Warnf("Failed to get services in ns %s", namespace) 267 | return nil, err 268 | } 269 | 270 | var handledReplicationControllers []string 271 | var wg sync.WaitGroup 272 | queue := make(chan *Website, 1) 273 | podLoop: 274 | for _, pod := range pods.Items { 275 | // skip pods in already active namespaces 276 | if namespace == "All Namespaces" { 277 | for _, n := range c.activeNamespaces { 278 | if n != namespace && n == pod.Namespace { 279 | continue podLoop 280 | } 281 | } 282 | } 283 | // skip not running pods 284 | if pod.Status.Phase != "Running" { 285 | continue podLoop 286 | } 287 | // has been scheduled from deletion 288 | if pod.DeletionTimestamp != nil { 289 | continue podLoop 290 | } 291 | // handle replication controllers we only need one pod from each replica 292 | for _, owner := range pod.OwnerReferences { 293 | if owner.Kind == "StatefulSet" || owner.Kind == "ReplicaSet" || owner.Kind == "DaemonSet" { 294 | for _, rc := range handledReplicationControllers { 295 | if rc == owner.Name { 296 | continue podLoop 297 | } 298 | } 299 | handledReplicationControllers = append(handledReplicationControllers, owner.Name) 300 | } 301 | } 302 | // services 303 | handledPorts := c.handleServicesInPod(services, pod, &wg, queue) 304 | // container ports 305 | c.handleContainerPortsInPod(pod, handledPorts, &wg, queue) 306 | } 307 | go func() { 308 | for w := range queue { 309 | c.log.Infof("received website forwarded (%t) to port %d from chan!", w.isForwarded, w.LocalPort) 310 | if w.isForwarded { 311 | nsWebsites = append(nsWebsites, w) 312 | } 313 | wg.Done() 314 | } 315 | }() 316 | 317 | c.log.Infof("waiting for all potential websites to be processed") 318 | wg.Wait() 319 | c.log.Infof("%d websites processed", len(nsWebsites)) 320 | return nsWebsites, nil 321 | } 322 | 323 | func (c *Client) addDerivedDetailsToWebsites() { 324 | for _, website := range c.websites { 325 | if website.Title == "" { 326 | website.Title = website.icon.PageTitle 327 | if website.Title == "" { 328 | website.Title = website.portForwardReq.Pod.Name 329 | } 330 | website.IconUrl = fmt.Sprintf("file://%s", website.icon.FilePath) 331 | website.IconRemoteUrl = website.icon.RemoteUrl 332 | website.PodName = website.portForwardReq.Pod.Name 333 | website.Namespace = website.portForwardReq.Pod.Namespace 334 | } 335 | } 336 | } 337 | 338 | // GetWebsitesInNamespace takes a namespace's name and ensures that all websites in that namespace are port-forwarded. 339 | // If the namespaces in the Website are not port-forwarded then forwardAndGetIconsForWebsitesInNamespace is called. 340 | // Finally a json response of a list of Websites for the namespace specified is returned. 341 | func (c *Client) GetWebsitesInNamespace(namespace string) string { 342 | skip := false 343 | if namespace != "All Namespaces" { 344 | for _, ns := range c.activeNamespaces { 345 | if ns == "All Namespaces" || ns == namespace { 346 | skip = true 347 | break 348 | } 349 | } 350 | } 351 | 352 | var nsWebsites []*Website 353 | var err error 354 | if !skip { 355 | nsWebsites, err = c.forwardAndGetIconsForWebsitesInNamespace(namespace) 356 | if err != nil { 357 | return "" 358 | } 359 | c.log.Infof("Got %d websites forwarded in ns %s", len(nsWebsites), namespace) 360 | c.websites = append(c.websites, nsWebsites...) 361 | c.addDerivedDetailsToWebsites() 362 | } else { 363 | c.log.Infof("skipping get websites for namespace %s as already in active namespaces %v", namespace, c.activeNamespaces) 364 | for _, w := range c.websites { 365 | if w.portForwardReq.Pod.Namespace == namespace { 366 | nsWebsites = append(nsWebsites, w) 367 | } 368 | } 369 | } 370 | c.activeNamespaces = append(c.activeNamespaces, namespace) 371 | 372 | jBytes, _ := json.Marshal(nsWebsites) 373 | return string(jBytes) 374 | } 375 | 376 | // getDefaultClientSetAndConfig is an initialization method to get the default kubernetes config and initialize 377 | // the Clientset 378 | func getDefaultClientSetAndConfig() (*kubernetes.Clientset, *rest.Config, *api.Config, string, error) { 379 | var configPath string 380 | if home := homeDir(); home != "" { 381 | configPath = filepath.Join(home, ".kube", "config") 382 | rawConfig, err := clientcmd.LoadFromFile(configPath) 383 | if err != nil { 384 | return &kubernetes.Clientset{}, &rest.Config{}, &api.Config{}, "", err 385 | } 386 | for k := range rawConfig.Contexts { 387 | rawConfig.CurrentContext = k 388 | } 389 | 390 | clientConf := clientcmd.NewNonInteractiveClientConfig(*rawConfig, rawConfig.CurrentContext, 391 | &clientcmd.ConfigOverrides{}, clientcmd.NewDefaultClientConfigLoadingRules()) 392 | restConf, err := clientConf.ClientConfig() 393 | if err != nil { 394 | return &kubernetes.Clientset{}, &rest.Config{}, &api.Config{}, configPath, err 395 | } 396 | clientSet, err := kubernetes.NewForConfig(restConf) 397 | if err != nil { 398 | return &kubernetes.Clientset{}, &rest.Config{}, &api.Config{}, configPath, err 399 | } 400 | return clientSet, restConf, rawConfig, configPath, nil 401 | } 402 | return &kubernetes.Clientset{}, &rest.Config{}, &api.Config{}, configPath, errors.New("default config not found") 403 | } 404 | 405 | // GetCurrentConfigPath simply returns the configPath 406 | func (c *Client) GetCurrentConfigPath() string { 407 | return c.configPath 408 | } 409 | 410 | // GetAvailableContexts gets a slice of available contexts for the current conf 411 | func (c *Client) GetAvailableContexts() []string { 412 | contexts := make([]string, len(c.rawConf.Contexts)) 413 | i := 0 414 | for k := range c.rawConf.Contexts { 415 | contexts[i] = k 416 | i++ 417 | } 418 | return contexts 419 | } 420 | 421 | // GetCurrentContext returns the context active for the current conf 422 | func (c *Client) GetCurrentContext() string { 423 | return c.currentContext 424 | } 425 | 426 | // SetConfigPath takes a configPath string and tries to configure the Client for that config. If it was successful 427 | // the configPath is returned. Otherwise the old configPath is returned. 428 | func (c *Client) SetConfigPath(configPath string, context string) []string { 429 | var rawConfig *api.Config 430 | var err error 431 | var useContext string 432 | if configPath != c.configPath { 433 | rawConfig, err = clientcmd.LoadFromFile(configPath) 434 | if err != nil { 435 | c.log.Debugf("%v", err) 436 | return []string{c.configPath, c.currentContext} 437 | } 438 | for k := range rawConfig.Contexts { 439 | useContext = k 440 | break 441 | } 442 | } else { 443 | rawConfig = c.rawConf 444 | useContext = context 445 | // nothing changed 446 | if useContext == c.currentContext { 447 | return []string{c.configPath, c.currentContext} 448 | } 449 | } 450 | 451 | clientConf := clientcmd.NewNonInteractiveClientConfig(*rawConfig, useContext, &clientcmd.ConfigOverrides{}, 452 | clientcmd.NewDefaultClientConfigLoadingRules()) 453 | restConf, err := clientConf.ClientConfig() 454 | 455 | if err != nil { 456 | c.log.Infof("error building restConf from path %s", configPath) 457 | c.log.Debugf("%v", err) 458 | return []string{c.configPath, c.currentContext} 459 | } 460 | clientSet, err := kubernetes.NewForConfig(restConf) 461 | if err != nil { 462 | c.log.Infof("error building clientset from restConf at %s", configPath) 463 | return []string{c.configPath, c.currentContext} 464 | } 465 | 466 | // close forwards in the old context 467 | c.closeAllPortForwards() 468 | c.rawConf = rawConfig 469 | c.currentContext = useContext 470 | c.s = clientSet 471 | c.configPath = configPath 472 | c.conf = restConf 473 | return []string{configPath, useContext} 474 | } 475 | 476 | func (c *Client) closeAllPortForwards() { 477 | for _, w := range c.websites { 478 | c.log.Infof("closing port forward on port %d of pod %s", w.PodPort, w.portForwardReq.Pod.Name) 479 | close(w.portForwardReq.StopCh) 480 | } 481 | } 482 | 483 | // todo: search for all config files and present them in ui - autodetect 484 | 485 | func homeDir() string { 486 | if h := os.Getenv("HOME"); h != "" { 487 | return h 488 | } 489 | return os.Getenv("USERPROFILE") // windows 490 | } 491 | 492 | // WailsInit takes the wails runtime and does some initialization - sets up the default client if possible 493 | func (c *Client) WailsInit(runtime *wails.Runtime) error { 494 | c.log = logger.NewCustomLogger("Client", runtime) 495 | s, conf, rawConf, confPath, err := getDefaultClientSetAndConfig() 496 | if err != nil { 497 | c.log.Warnf("failed to get default config: %v", err.Error()) 498 | return nil 499 | } 500 | namespaces, err := s.CoreV1().Namespaces().List(metav1.ListOptions{}) 501 | if err != nil || len(namespaces.Items) == 0 { 502 | c.rawConf = rawConf 503 | c.currentContext = c.rawConf.CurrentContext 504 | c.configPath = confPath 505 | c.s = s 506 | c.conf = conf 507 | c.log.Infof("no namespaces in cluster with config path %s - could be a connection issue", confPath) 508 | return nil 509 | } 510 | c.rawConf = rawConf 511 | c.currentContext = c.rawConf.CurrentContext 512 | c.s = s 513 | c.conf = conf 514 | c.configPath = confPath 515 | 516 | return nil 517 | } 518 | 519 | // WailsShutdown is called on shutdown and cleans up all port-forwards still active 520 | func (c *Client) WailsShutdown() { 521 | c.closeAllPortForwards() 522 | } 523 | -------------------------------------------------------------------------------- /pkg/favicon/favicon.go: -------------------------------------------------------------------------------- 1 | package favicon 2 | 3 | // draws heavily on Scott Werner's python package https://github.com/scottwernervt/favicon 4 | // todo: separate into its own module 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "github.com/PuerkitoBio/goquery" 10 | "io" 11 | "io/ioutil" 12 | "log" 13 | "mime" 14 | "net/http" 15 | "net/url" 16 | "path" 17 | "regexp" 18 | "sort" 19 | "strconv" 20 | "strings" 21 | "time" 22 | ) 23 | 24 | // Icon describes a favicon or other icon for a given website 25 | type Icon struct { 26 | width int 27 | height int 28 | RemoteUrl string 29 | FilePath string 30 | mimeType string 31 | size int64 32 | PageTitle string 33 | } 34 | 35 | var linkRels = [4]string{"icon", "shortcut icon", "apple-touch-icon", "apple-touch-icon-precomposed"} 36 | var metaNames = [3]string{"msapplication-TileImage", "og:image", "image"} 37 | 38 | // GetBest takes a url and gets the best Icon for it (where best is defined as largest file size) 39 | func GetBest(getUrl string) (*Icon, error) { 40 | parsedUrl, err := url.Parse(getUrl) 41 | if err != nil { 42 | return nil, err 43 | } 44 | req := http.Request{ 45 | Method: "GET", 46 | URL: parsedUrl, 47 | Proto: "HTTP", 48 | } 49 | c := http.Client{ 50 | Timeout: 4 * time.Second, 51 | } 52 | resp, err := c.Do(&req) 53 | if err != nil { 54 | return nil, err 55 | } 56 | defer resp.Body.Close() 57 | if resp.StatusCode >= 400 { 58 | return nil, errors.New("received bad status code") 59 | } 60 | 61 | var icons []*Icon 62 | respUrl := resp.Request.URL 63 | defIco, err := defaultIcon(respUrl) 64 | if err == nil { 65 | icons = append(icons, defIco) 66 | } 67 | doc, err := goquery.NewDocumentFromReader(resp.Body) 68 | if err != nil { 69 | log.Print(err) 70 | return nil, err 71 | } 72 | tmIcons, err := tagMetaIcons(*doc, *respUrl) 73 | if err == nil { 74 | icons = append(icons, tmIcons...) 75 | } 76 | if len(icons) == 0 { 77 | return nil, errors.New("failed to get any icons for website") 78 | } 79 | log.Printf("favicon finder got a total of %d icons to choose from", len(icons)) 80 | 81 | // get the largest favicon 82 | sort.Slice(icons, func(i, j int) bool { 83 | return icons[i].size > icons[j].size 84 | }) 85 | 86 | bestIcon := icons[0] 87 | bestIcon.PageTitle = getTitle(*doc, *respUrl) 88 | return bestIcon, nil 89 | } 90 | 91 | func getTitle(doc goquery.Document, respUrl url.URL) string { 92 | title := strings.TrimSpace(doc.Find("title").First().Text()) 93 | log.Printf("Got title '%s' for page at %s", title, respUrl.String()) 94 | return title 95 | } 96 | 97 | func getTagsToFetch(doc goquery.Document) []*goquery.Selection { 98 | var tagsToFetch []*goquery.Selection 99 | // handle link nodes 100 | doc.Find("link").Each(func(i int, s *goquery.Selection) { 101 | rel, exists := s.Attr("rel") 102 | if !exists { 103 | return 104 | } 105 | rel = strings.ToLower(rel) 106 | for _, lr := range linkRels { 107 | if rel == lr { 108 | tagsToFetch = append(tagsToFetch, s) 109 | return 110 | 111 | } 112 | } 113 | }) 114 | // handle meta nodes 115 | doc.Find("meta").Each(func(i int, s *goquery.Selection) { 116 | metaType, exists := s.Attr("name") 117 | if !exists { 118 | metaType, exists = s.Attr("property") 119 | if !exists { 120 | metaType, exists = s.Attr("itemprop") 121 | if !exists { 122 | return 123 | } 124 | } 125 | } 126 | metaType = strings.ToLower(metaType) 127 | for _, mn := range metaNames { 128 | if metaType == strings.ToLower(mn) { 129 | tagsToFetch = append(tagsToFetch, s) 130 | } 131 | } 132 | }) 133 | 134 | return tagsToFetch 135 | } 136 | 137 | func tagMetaIcons(doc goquery.Document, respUrl url.URL) ([]*Icon, error) { 138 | tagsToFetch := getTagsToFetch(doc) 139 | log.Printf("Got %d icon tags for RemoteUrl %s", len(tagsToFetch), respUrl.String()) 140 | 141 | var icons []*Icon 142 | for _, s := range tagsToFetch { 143 | linkUrl, err := tagMetaUrlGetter(s) 144 | if err != nil { 145 | continue 146 | } 147 | trimmedLinkUrl := strings.TrimSpace(*linkUrl) 148 | if trimmedLinkUrl == "" || strings.HasPrefix(trimmedLinkUrl, "data:image/") { 149 | continue 150 | } 151 | 152 | parsedLinkUrl, err := url.Parse(trimmedLinkUrl) 153 | if err != nil { 154 | continue 155 | } 156 | 157 | if !parsedLinkUrl.IsAbs() { 158 | parsedLinkUrl.Host = respUrl.Host 159 | parsedLinkUrl.Scheme = respUrl.Scheme 160 | if strings.HasPrefix(parsedLinkUrl.Path, ".") { 161 | parsedLinkUrl.Path = path.Join(respUrl.Path, parsedLinkUrl.Path) 162 | } 163 | } 164 | 165 | width, height := dimensions(s, linkUrl) 166 | 167 | downloadedIcon, err := getIconFromUrl(parsedLinkUrl) 168 | if err != nil { 169 | continue 170 | } 171 | icons = append(icons, &Icon{ 172 | width: width, 173 | height: height, 174 | RemoteUrl: downloadedIcon.RemoteUrl, 175 | FilePath: downloadedIcon.FilePath, 176 | mimeType: downloadedIcon.mimeType, 177 | size: downloadedIcon.size, 178 | }) 179 | } 180 | return icons, nil 181 | } 182 | 183 | func dimensions(t *goquery.Selection, linkUrl *string) (int, int) { 184 | // Get icon dimensions from size attribute or filename 185 | sizes, exists := t.Attr("sizes") 186 | var width, height string 187 | if exists && sizes != "any" { 188 | size := strings.Split(sizes, " ") 189 | sort.Sort(sort.Reverse(sort.StringSlice(size))) 190 | re := regexp.MustCompile(`(?m)[x\xd7]`) 191 | unpack(re.Split(size[0], 2), &width, &height) 192 | } else { 193 | sizeRE := regexp.MustCompile("(?mi)(?P[[:digit:]]{2,4})x(?P[[:digit:]]{2,4})") 194 | matches := sizeRE.FindStringSubmatch(*linkUrl) 195 | if matches != nil && len(matches) == 2 { 196 | unpack(matches, &width, &height) 197 | } else { 198 | width, height = "0", "0" 199 | } 200 | 201 | } 202 | // repair bad attribute values e.g. 192x192+ 203 | numRE := regexp.MustCompile("[0-9]+") 204 | width = numRE.FindString(width) 205 | height = numRE.FindString(height) 206 | 207 | widthInt, err := strconv.Atoi(width) 208 | if err != nil { 209 | widthInt = 0 210 | } 211 | heightInt, err := strconv.Atoi(height) 212 | if err != nil { 213 | heightInt = 0 214 | } 215 | // todo: support og:image:width content=1000 - requires doc level lookups 216 | 217 | return widthInt, heightInt 218 | } 219 | 220 | func tagMetaUrlGetter(tm *goquery.Selection) (*string, error) { 221 | href, exists := tm.Attr("href") 222 | if exists { 223 | return &href, nil 224 | } 225 | content, exists := tm.Attr("content") 226 | if exists { 227 | return &content, nil 228 | } 229 | return nil, errors.New("no RemoteUrl found on html tag") 230 | } 231 | 232 | func responseToFile(response http.Response, extension string) (string, *int64) { 233 | file, err := ioutil.TempFile("", fmt.Sprintf("portfall*%s", extension)) 234 | if err != nil { 235 | log.Fatal(err) 236 | } 237 | defer file.Close() 238 | size, err := io.Copy(file, response.Body) 239 | if err != nil { 240 | log.Fatal(err) 241 | } 242 | return file.Name(), &size 243 | } 244 | 245 | func defaultIcon(parsedUrl *url.URL) (*Icon, error) { 246 | faviconUrl := url.URL{ 247 | Scheme: parsedUrl.Scheme, 248 | Host: parsedUrl.Host, 249 | Path: "favicon.ico", 250 | } 251 | icon, err := getIconFromUrl(&faviconUrl) 252 | return icon, err 253 | } 254 | 255 | func getIconFromUrl(iconUrl *url.URL) (*Icon, error) { 256 | // download icon and get extension and size 257 | log.Printf("Getting icon from RemoteUrl %s", iconUrl.String()) 258 | c := http.Client{ 259 | Timeout: 3 * time.Second, 260 | } 261 | resp, err := c.Get(iconUrl.String()) 262 | if err != nil { 263 | return nil, err 264 | } 265 | if resp.StatusCode >= 400 { 266 | return nil, fmt.Errorf( 267 | "received bad status code %d attempting to retrieve icon at %s", resp.StatusCode, iconUrl.String()) 268 | } 269 | defer resp.Body.Close() 270 | 271 | mimeType := resp.Header.Get("content-type") 272 | extensions, err := mime.ExtensionsByType(mimeType) 273 | if err != nil { 274 | return nil, err 275 | } 276 | var extension string 277 | if extensions != nil { 278 | extension = extensions[0] 279 | } 280 | if !strings.HasPrefix(mimeType, "image") { 281 | return nil, fmt.Errorf("bad mimeType %s on remote icon", mimeType) 282 | } 283 | 284 | fp, size := responseToFile(*resp, extension) 285 | return &Icon{ 286 | width: 0, 287 | height: 0, 288 | RemoteUrl: iconUrl.String(), 289 | FilePath: fp, 290 | mimeType: mimeType, 291 | size: *size, 292 | }, nil 293 | } 294 | 295 | func unpack(s []string, vars ...*string) { 296 | for i, str := range s { 297 | *vars[i] = str 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /pkg/favicon/favicon_test.go: -------------------------------------------------------------------------------- 1 | package favicon 2 | 3 | import ( 4 | "github.com/PuerkitoBio/goquery" 5 | "net/http" 6 | "net/url" 7 | "os" 8 | "path" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | // todo: create a mock server for these reqs to hit 14 | 15 | func TestDefaultIcon(t *testing.T) { 16 | goog, _ := url.Parse("http://www.google.com") 17 | icon, err := defaultIcon(goog) 18 | if err != nil || icon == nil { 19 | t.Error(err) 20 | return 21 | } 22 | if icon.RemoteUrl != "http://www.google.com/favicon.ico" { 23 | t.Errorf("icon RemoteUrl wrong, expected http://www.google.com/favicon.ico, got %s", icon.RemoteUrl) 24 | } 25 | if path.Dir(icon.FilePath) != os.TempDir() { 26 | t.Errorf("expected output to live in os tempdir %s got %s", os.TempDir(), path.Dir(icon.FilePath)) 27 | } 28 | if icon.size == 0 { 29 | t.Errorf("expected non-zero file iconsize") 30 | } 31 | } 32 | 33 | func TestTagMetaIcons(t *testing.T) { 34 | goog, _ := url.Parse("http://www.google.com") 35 | resp, err := http.DefaultClient.Get(goog.String()) 36 | if err != nil { 37 | t.Errorf("Connectivity problem") 38 | } 39 | defer resp.Body.Close() 40 | doc, err := goquery.NewDocumentFromReader(resp.Body) 41 | if err != nil { 42 | t.Error(err) 43 | } 44 | tmIcons, err := tagMetaIcons(*doc, *resp.Request.URL) 45 | if err != nil { 46 | t.Error(err) 47 | } 48 | if len(tmIcons) != 1 { 49 | t.Errorf("Got %d icons, expected 1", len(tmIcons)) 50 | }else{ 51 | icon := tmIcons[0] 52 | if icon.size == 0 { 53 | t.Errorf("expected non-zero file iconsize") 54 | } 55 | if path.Dir(icon.FilePath) != os.TempDir() { 56 | t.Errorf("expected output to live in os tempdir %s got %s", os.TempDir(), path.Dir(icon.FilePath)) 57 | } 58 | if icon.width != 0 || icon.height != 0 { 59 | t.Errorf("expected 0,0 height,width. Got %d,%d", icon.width, icon.height) 60 | } 61 | } 62 | 63 | 64 | } 65 | 66 | 67 | func TestTagMetaIconsWithNoFavicon(t *testing.T) { 68 | raw := ` 69 | 70 | 71 | 72 | 85 | 86 | 87 | 88 | 89 | 90 | Grafana 91 | 92 | 93 | 94 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | ` 120 | reader := strings.NewReader(raw) 121 | doc, err := goquery.NewDocumentFromReader(reader) 122 | if err != nil { 123 | t.Error(err) 124 | } 125 | tmIcons, err := tagMetaIcons(*doc, url.URL{ 126 | Scheme: "https", 127 | Host: "play.grafana.org", 128 | }) 129 | if err != nil { 130 | t.Error(err) 131 | } 132 | if len(tmIcons) != 2 { 133 | t.Errorf("Got %d icons, expected 1", len(tmIcons)) 134 | } 135 | 136 | icon := tmIcons[0] 137 | if icon.size == 0 { 138 | t.Errorf("expected non-zero file iconsize") 139 | } 140 | if path.Dir(icon.FilePath) != os.TempDir() { 141 | t.Errorf("expected output to live in os tempdir %s got %s", os.TempDir(), path.Dir(icon.FilePath)) 142 | } 143 | if icon.width != 0 || icon.height != 0 { 144 | t.Errorf("expected 0,0 height,width. Got %d,%d", icon.width, icon.height) 145 | } 146 | } 147 | 148 | func TestGetTitle(t *testing.T) { 149 | goog, _ := url.Parse("http://www.google.com") 150 | resp, err := http.DefaultClient.Get(goog.String()) 151 | if err != nil { 152 | t.Error(err) 153 | } 154 | defer resp.Body.Close() 155 | doc, err := goquery.NewDocumentFromReader(resp.Body) 156 | if err != nil { 157 | t.Error(err) 158 | } 159 | title := getTitle(*doc, *resp.Request.URL) 160 | if title != "Google" { 161 | t.Errorf("Expected page title Google but got %s", title) 162 | } 163 | } 164 | 165 | func TestGetBest(t *testing.T) { 166 | goog, _ := url.Parse("http://www.google.com") 167 | icon, err := GetBest(goog.String()) 168 | if err != nil { 169 | t.Error(err) 170 | } 171 | if icon.size == 0 { 172 | t.Errorf("expected non-zero file iconsize") 173 | } 174 | if path.Dir(icon.FilePath) != os.TempDir() { 175 | t.Errorf("expected output to live in os tempdir %s got %s", os.TempDir(), path.Dir(icon.FilePath)) 176 | } 177 | if icon.width != 0 || icon.height != 0 { 178 | t.Errorf("expected 0,0 height,width. Got %d,%d", icon.width, icon.height) 179 | } 180 | 181 | defaultIcon, err := defaultIcon(goog) 182 | if err != nil { 183 | t.Error(err) 184 | } 185 | // best icon is actually chosen 186 | if defaultIcon.RemoteUrl == icon.RemoteUrl || defaultIcon.size == icon.size { 187 | t.Errorf("Best icon not chosen. Favicon chosen.") 188 | } 189 | if icon.PageTitle != "Google" { 190 | t.Errorf("Expected page title Google but got %s", icon.PageTitle) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /pkg/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "github.com/wailsapp/wails" 6 | l "github.com/wailsapp/wails/lib/logger" 7 | "sync" 8 | ) 9 | 10 | // Custom logger implements wails' logging but emits all logs to frontend 11 | type CustomLogger struct { 12 | prefix string 13 | mu sync.Mutex 14 | runtime *wails.Runtime 15 | errorOnly bool 16 | } 17 | 18 | // NewCustomLogger creates a new custom logger with the given prefix 19 | func NewCustomLogger(prefix string, runtime *wails.Runtime) *CustomLogger { 20 | l.SetLogLevel("debug") 21 | return &CustomLogger{ 22 | prefix: "[" + prefix + "] ", 23 | runtime: runtime, 24 | } 25 | } 26 | 27 | type logMessage struct { 28 | prefix string 29 | message string 30 | fields l.Fields 31 | } 32 | 33 | // Info level message 34 | func (c *CustomLogger) Info(message string) { 35 | l.GlobalLogger.Info(c.prefix + message) 36 | c.mu.Lock() 37 | defer c.mu.Unlock() 38 | c.runtime.Events.Emit("info", fmt.Sprintf("%s%s", c.prefix, message)) 39 | } 40 | 41 | // Infof - formatted message 42 | func (c *CustomLogger) Infof(message string, args ...interface{}) { 43 | l.GlobalLogger.Infof(c.prefix+message, args...) 44 | c.mu.Lock() 45 | defer c.mu.Unlock() 46 | c.runtime.Events.Emit("log:info", fmt.Sprintf("%s%s", c.prefix, fmt.Sprintf(message, args...))) 47 | } 48 | 49 | // InfoFields - message with fields 50 | func (c *CustomLogger) InfoFields(message string, fields l.Fields) { 51 | l.GlobalLogger.WithFields(map[string]interface{}(fields)).Info(c.prefix + message) 52 | c.mu.Lock() 53 | defer c.mu.Unlock() 54 | c.runtime.Events.Emit("log:info", fmt.Sprintf("%s%s", c.prefix, message)) 55 | } 56 | 57 | // Debug level message 58 | func (c *CustomLogger) Debug(message string) { 59 | l.GlobalLogger.Debug(c.prefix + message) 60 | c.mu.Lock() 61 | defer c.mu.Unlock() 62 | c.runtime.Events.Emit("log:debug", fmt.Sprintf("%s%s", c.prefix, message)) 63 | } 64 | 65 | // Debugf - formatted message 66 | func (c *CustomLogger) Debugf(message string, args ...interface{}) { 67 | l.GlobalLogger.Debugf(c.prefix+message, args...) 68 | c.mu.Lock() 69 | defer c.mu.Unlock() 70 | c.runtime.Events.Emit("log:debug", fmt.Sprintf("%s%s", c.prefix, fmt.Sprintf(message, args...))) 71 | } 72 | 73 | // DebugFields - message with fields 74 | func (c *CustomLogger) DebugFields(message string, fields l.Fields) { 75 | l.GlobalLogger.WithFields(map[string]interface{}(fields)).Debug(c.prefix + message) 76 | c.mu.Lock() 77 | defer c.mu.Unlock() 78 | c.runtime.Events.Emit("log:debug", fmt.Sprintf("%s%s", c.prefix, message)) 79 | } 80 | 81 | // Warn level message 82 | func (c *CustomLogger) Warn(message string) { 83 | l.GlobalLogger.Warn(c.prefix + message) 84 | c.mu.Lock() 85 | defer c.mu.Unlock() 86 | c.runtime.Events.Emit("log:warn", fmt.Sprintf("%s%s", c.prefix, message)) 87 | } 88 | 89 | // Warnf - formatted message 90 | func (c *CustomLogger) Warnf(message string, args ...interface{}) { 91 | l.GlobalLogger.Warnf(c.prefix+message, args...) 92 | c.mu.Lock() 93 | defer c.mu.Unlock() 94 | c.runtime.Events.Emit("log:warn", fmt.Sprintf("%s%s", c.prefix, fmt.Sprintf(message, args...))) 95 | } 96 | 97 | // WarnFields - message with fields 98 | func (c *CustomLogger) WarnFields(message string, fields l.Fields) { 99 | l.GlobalLogger.WithFields(map[string]interface{}(fields)).Warn(c.prefix + message) 100 | c.mu.Lock() 101 | defer c.mu.Unlock() 102 | c.runtime.Events.Emit("log:warn", fmt.Sprintf("%s%s", c.prefix, message)) 103 | } 104 | 105 | // Error level message 106 | func (c *CustomLogger) Error(message string) { 107 | l.GlobalLogger.Error(c.prefix + message) 108 | c.mu.Lock() 109 | defer c.mu.Unlock() 110 | c.runtime.Events.Emit("log:error", fmt.Sprintf("%s%s", c.prefix, message)) 111 | } 112 | 113 | // Errorf - formatted message 114 | func (c *CustomLogger) Errorf(message string, args ...interface{}) { 115 | l.GlobalLogger.Errorf(c.prefix+message, args...) 116 | c.mu.Lock() 117 | defer c.mu.Unlock() 118 | c.runtime.Events.Emit("log:error", fmt.Sprintf("%s%s", c.prefix, fmt.Sprintf(message, args...))) 119 | } 120 | 121 | // ErrorFields - message with fields 122 | func (c *CustomLogger) ErrorFields(message string, fields l.Fields) { 123 | l.GlobalLogger.WithFields(map[string]interface{}(fields)).Error(c.prefix + message) 124 | c.mu.Lock() 125 | defer c.mu.Unlock() 126 | c.runtime.Events.Emit("log:error", fmt.Sprintf("%s%s", c.prefix, message)) 127 | } 128 | 129 | // Fatal level message 130 | func (c *CustomLogger) Fatal(message string) { 131 | l.GlobalLogger.Fatal(c.prefix + message) 132 | c.mu.Lock() 133 | defer c.mu.Unlock() 134 | c.runtime.Events.Emit("log:fatal", fmt.Sprintf("%s%s", c.prefix, message)) 135 | } 136 | 137 | // Fatalf - formatted message 138 | func (c *CustomLogger) Fatalf(message string, args ...interface{}) { 139 | l.GlobalLogger.Fatalf(c.prefix+message, args...) 140 | c.mu.Lock() 141 | defer c.mu.Unlock() 142 | c.runtime.Events.Emit("log:fatal", fmt.Sprintf("%s%s", c.prefix, fmt.Sprintf(message, args...))) 143 | } 144 | 145 | // FatalFields - message with fields 146 | func (c *CustomLogger) FatalFields(message string, fields l.Fields) { 147 | l.GlobalLogger.WithFields(map[string]interface{}(fields)).Fatal(c.prefix + message) 148 | c.mu.Lock() 149 | defer c.mu.Unlock() 150 | c.runtime.Events.Emit("log:fatal", fmt.Sprintf("%s%s", c.prefix, message)) 151 | } -------------------------------------------------------------------------------- /pkg/os/os.go: -------------------------------------------------------------------------------- 1 | package os 2 | 3 | import ( 4 | "github.com/pkg/browser" 5 | "github.com/wailsapp/wails" 6 | "portfall/pkg/logger" 7 | ) 8 | 9 | // PortfallOS manages os related functionality such as opening files or browsers 10 | type PortfallOS struct { 11 | rt *wails.Runtime 12 | log *logger.CustomLogger 13 | } 14 | 15 | // OpenFile opens the system dialog to get a file and return it to the frontend 16 | func (p *PortfallOS) OpenFile() string { 17 | file := p.rt.Dialog.SelectFile() 18 | return file 19 | } 20 | 21 | // OpenInBrowser opens the operating system browser at the specified url 22 | func (p *PortfallOS) OpenInBrowser(openUrl string) { 23 | err := browser.OpenURL(openUrl) 24 | if err != nil { 25 | p.log.Errorf("%v", err) 26 | } 27 | } 28 | 29 | func (p *PortfallOS) GetVersion() string { 30 | //bi, ok := debug.ReadBuildInfo() 31 | //if !ok { 32 | // p.log.Warn("Could not get build info") 33 | //} 34 | //p.log.Debugf("Got version %s", bi.Main.Version) 35 | //return bi.Main.Version 36 | return "v0.8.4" 37 | } 38 | 39 | // WailsInit assigns the runtime to the PortfallOS struct 40 | func (p *PortfallOS) WailsInit(runtime *wails.Runtime) error { 41 | p.rt = runtime 42 | p.log = logger.NewCustomLogger("PortfallOS", runtime) 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Portfall", 3 | "description": "Enter your project description", 4 | "author": { 5 | "name": "Reuben Thomas-Davis", 6 | "email": "reuben@rekon.uk" 7 | }, 8 | "version": "v0.4.0", 9 | "binaryname": "Portfall", 10 | "frontend": { 11 | "dir": "frontend", 12 | "install": "npm install", 13 | "build": "npm run build", 14 | "bridge": "src", 15 | "serve": "npm run start" 16 | }, 17 | "WailsVersion": "v1.0.2" 18 | } -------------------------------------------------------------------------------- /snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: portfall 2 | version: git 3 | summary: A tiny k8s port-forwarding portal for easy access to all your cluster UIs. 4 | description: | 5 | Portfall offers a unified UI for accessing the webapps in your Kubernetes clusters through port-forwarding. 6 | confinement: classic 7 | base: core18 8 | icon: appicon.png 9 | title: Portfall 10 | license: MIT 11 | 12 | parts: 13 | portfall: 14 | plugin: go 15 | go-importpath: github.com/rekon-oss/portfall 16 | source: . 17 | source-type: git 18 | build-snaps: 19 | - node/13/stable 20 | build-packages: 21 | - gcc 22 | - libgtk-3-dev 23 | - libwebkit2gtk-4.0-dev 24 | override-build: | 25 | go get -u github.com/wailsapp/wails/cmd/wails 26 | PATH="$PATH:/root/go/bin" 27 | wails build 28 | mkdir -p $SNAPCRAFT_PART_INSTALL/bin 29 | cp Portfall $SNAPCRAFT_PART_INSTALL/bin/ 30 | mkdir -pv $SNAPCRAFT_PART_INSTALL/meta/gui/ 31 | cp appicon.png $SNAPCRAFT_PART_INSTALL/meta/gui/ 32 | cp Portfall.desktop $SNAPCRAFT_PART_INSTALL/usr/share/applications/ 33 | stage-packages: 34 | - libgtk-3-dev 35 | - libwebkit2gtk-4.0-dev 36 | apps: 37 | portfall: 38 | command: bin/Portfall 39 | desktop: usr/share/applications/Portfall.desktop 40 | 41 | architectures: 42 | - amd64 43 | - arm64 44 | - i386 45 | - armhf 46 | 47 | # -- confinement: strict elements -- 48 | #plugs: 49 | # fs-access: 50 | # interface: system-files 51 | # read: 52 | # - /tmp 53 | # write: 54 | # - /tmp 55 | # 56 | #plugs: 57 | # config-foo: 58 | # interface: personal-files 59 | # read: 60 | # - $HOME/.config/k3d 61 | # - $HOME/.kube 62 | # - $HOME/.minikube/config/ 63 | #apps: 64 | # portfall: 65 | # extensions: [gnome-3-28] 66 | # slots: 67 | # - dbus-daemon 68 | #slots: 69 | # dbus-daemon: 70 | # interface: dbus 71 | # bus: session 72 | # name: com.github.rekon-oss.portfall 73 | 74 | #layout: 75 | # /usr/lib/$SNAPCRAFT_ARCH_TRIPLET/webkit2gtk-4.0: 76 | # bind: $SNAP/gnome-platform/usr/lib/$SNAPCRAFT_ARCH_TRIPLET/webkit2gtk-4.0 77 | -------------------------------------------------------------------------------- /wails.json: -------------------------------------------------------------------------------- 1 | {"email":"reuben@rekon.uk","name":"Reuben Thomas-Davis"} --------------------------------------------------------------------------------