├── .github └── workflows │ └── version-release.yaml ├── .gitignore ├── .goreleaser.yaml ├── .vscode └── launch.json ├── LICENSE ├── app.json ├── cmd └── devops │ ├── config.yaml │ ├── install.go │ ├── main.go │ └── semantic.go ├── commands.sh ├── common ├── conn_init.go ├── monitoring.go └── release.go ├── devops-frontend ├── .vscode │ └── launch.json ├── README.md ├── commands.sh ├── index.html ├── openapitools.json ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ ├── helmicon-32.png │ │ ├── helmicon-64.png │ │ ├── kubernetes32.png │ │ └── react.svg │ ├── components │ │ ├── drawer │ │ │ ├── Drawer.css │ │ │ └── Drawer.tsx │ │ ├── infoCard │ │ │ ├── InfoCard.css │ │ │ └── InfoCard.tsx │ │ ├── isolator │ │ │ └── Isolator.tsx │ │ ├── recentlyUsed │ │ │ └── RecentlyUsed.tsx │ │ ├── resourceTable │ │ │ ├── ResourceTable.css │ │ │ └── ResourceTable.tsx │ │ ├── sideNav │ │ │ └── SideNav.tsx │ │ ├── specificActionForm │ │ │ └── SpecificActionForm.tsx │ │ └── xTerm │ │ │ └── XTerm.tsx │ ├── generated-sources │ │ └── openapi │ │ │ ├── .openapi-generator-ignore │ │ │ ├── .openapi-generator │ │ │ ├── FILES │ │ │ └── VERSION │ │ │ ├── apis │ │ │ ├── DefaultApi.ts │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── models │ │ │ ├── ModelAuthResponse.ts │ │ │ ├── ModelConfig.ts │ │ │ ├── ModelErrorResponse.ts │ │ │ ├── ModelEventResponse.ts │ │ │ ├── ModelFrontendEvent.ts │ │ │ ├── ModelInfoResponse.ts │ │ │ ├── ModelPlugin.ts │ │ │ ├── ModelServer.ts │ │ │ ├── ProtoAction.ts │ │ │ ├── ProtoAuthInfo.ts │ │ │ ├── ProtoExecution.ts │ │ │ ├── ProtoServerInput.ts │ │ │ ├── ProtoUserInput.ts │ │ │ ├── StructpbValue.ts │ │ │ └── index.ts │ │ │ └── runtime.ts │ ├── index.css │ ├── main.tsx │ ├── pages │ │ ├── Home.css │ │ ├── Home.tsx │ │ ├── PluginSelector.css │ │ └── PluginSelector.tsx │ ├── redux │ │ ├── actions │ │ │ └── infoActions.ts │ │ ├── reducers │ │ │ ├── Home.tsx │ │ │ ├── PluginSelectorReducer.tsx │ │ │ └── infoReducer.ts │ │ ├── store.ts │ │ └── typedReduxHook.ts │ ├── services │ │ ├── generalInfo.ts │ │ └── index.ts │ ├── types │ │ ├── Event.ts │ │ ├── InfoCardTypes.ts │ │ ├── ResourceTypes.ts │ │ └── utilsTypes.ts │ ├── utils │ │ ├── config.ts │ │ ├── constants.ts │ │ ├── notification.ts │ │ ├── settings.ts │ │ └── utils.ts │ └── vite-env.d.ts ├── swagger.yaml ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock ├── devops-plugin-sdk ├── devops_plugin.go ├── go.mod ├── go.sum ├── grpc_client.go ├── grpc_server.go ├── interface.go ├── logger.go ├── proto │ ├── devops.pb.go │ ├── devops.proto │ └── devops_grpc.pb.go └── types.go ├── docs ├── docs.go ├── swagger.json └── swagger.yaml ├── go.mod ├── go.sum ├── install.sh ├── internal ├── pluginmanager │ ├── current_plugin_context.go │ ├── helpers.go │ ├── normal_operations.go │ ├── operations.go │ ├── plugin_client.go │ ├── session.go │ ├── specific_operations.go │ └── table_stack.go ├── transformer │ ├── testdata │ │ └── testdata.go │ ├── transformer.go │ └── transformer_test.go └── tui │ ├── actions.go │ ├── application.go │ ├── delete_modal_page.go │ ├── event_handlers.go │ ├── eventloop.go │ ├── flash.go │ ├── form_page.go │ ├── general_info.go │ ├── helpers.go │ ├── http.go │ ├── isolator.go │ ├── main_page.go │ ├── plugin.go │ ├── search.go │ ├── specific_actions.go │ ├── start.go │ ├── table.go │ ├── text_only_page.go │ └── utils.go ├── model ├── config.go ├── events.go ├── http.go └── transformer.go ├── plugins ├── helm │ ├── .goreleaser.yaml │ ├── config.yaml │ ├── go.mod │ ├── go.sum │ ├── helm.go │ ├── implementation.go │ ├── main.go │ ├── operations.go │ ├── release.go │ └── resource_config │ │ ├── charts.yaml │ │ ├── defaults.yaml │ │ ├── releases.yaml │ │ └── repos.yaml └── kubernetes │ ├── .goreleaser.yaml │ ├── config.yaml │ ├── go.mod │ ├── go.sum │ ├── implementation.go │ ├── kubernetes.go │ ├── main.go │ ├── operations.go │ ├── release.go │ └── resource_config │ ├── configmaps.yaml │ ├── containers.yaml │ ├── cronjobs.yaml │ ├── daemonsets.yaml │ ├── defaults.yaml │ ├── deployments.yaml │ ├── jobs.yaml │ ├── namespaces.yaml │ ├── nodes.yaml │ ├── pods.yaml │ ├── replicasets.yaml │ ├── secrets.yaml │ ├── serviceaccounts.yaml │ └── services.yaml ├── readme.md ├── release.sh ├── server ├── handlers │ └── handlers.go ├── middleware.go └── server.go ├── utils ├── grpc.go ├── http.go ├── logger │ └── logger.go ├── template.go └── utils.go └── validator ├── docker.go ├── kubernetes.go ├── pods.go └── validation.go /.github/workflows/version-release.yaml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | - v* 7 | 8 | jobs: 9 | v2: 10 | name: "Version Relase" 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Setup stage golang environment 14 | uses: actions/setup-go@v4 15 | with: 16 | go-version: stable 17 | id: go 18 | 19 | - name: Check out code into the Go module directory 20 | uses: actions/checkout@v3 21 | 22 | - name: Cache Go Modules 23 | uses: actions/cache@v1 24 | with: 25 | path: ~/go/pkg/mod 26 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 27 | restore-keys: | 28 | ${{ runner.os }}-go- 29 | 30 | - name: Setup Nodejs and npm 31 | uses: actions/setup-node@v2 32 | with: 33 | node-version: "16" 34 | 35 | # - name: Build Frontend 36 | # run: | 37 | # cd devops-frontend 38 | # yarn install --frozen-lockfile 39 | # yarn build 40 | 41 | - name: Install goreleaser 42 | run: | 43 | wget -q https://github.com/goreleaser/goreleaser/releases/download/v1.17.2/goreleaser_Linux_x86_64.tar.gz 44 | tar -xzf goreleaser_Linux_x86_64.tar.gz 45 | sudo mv goreleaser /usr/local/bin/ 46 | 47 | - uses: ypicard/get-branch-name-github-action@v1 48 | id: current-branch 49 | 50 | - name: Set up Google Application Credentials 51 | run: | 52 | echo '${{ secrets.GOOGLE_APPLICATION_CREDENTIALS_JSON }}' > credentials.json 53 | echo 'GOOGLE_APPLICATION_CREDENTIALS=${{ github.workspace }}/credentials.json' >> $GITHUB_ENV 54 | 55 | - name: Run Build and Release script 56 | run: | 57 | # Download Go Releaser 58 | echo "Echo: Current branch is ${{ steps.current-branch.outputs.branch}}" 59 | 60 | # echo "Creating tag..." 61 | git config --global user.email "sharadregoti15@gmail.com" 62 | git config --global user.name "sharadregoti" 63 | git fetch origin ${{ steps.current-branch.outputs.branch}}:${{ steps.current-branch.outputs.branch}} 64 | git checkout ${{ steps.current-branch.outputs.branch}} 65 | git checkout -b "temp-branch" 66 | TIMESTAMP=$(date +%s) 67 | git tag -a "v0.5.3" -m "dummy tag" 68 | echo "TIMESTAMP=$TIMESTAMP" >> $GITHUB_ENV 69 | 70 | echo "Running script..." 71 | bash release.sh 72 | 73 | # - name: Run Build and Release script 74 | # run: | 75 | # # Download Go Releaser 76 | # echo "Echo: Current branch is ${{ steps.current-branch.outputs.branch}}" 77 | 78 | # # echo "Creating tag..." 79 | # git config --global user.email "sharadregoti15@gmail.com" 80 | # git config --global user.name "sharadregoti" 81 | # git fetch origin ${{ steps.current-branch.outputs.branch}}:${{ steps.current-branch.outputs.branch}} 82 | # git checkout ${{ steps.current-branch.outputs.branch}} 83 | # git tag -a ${{ steps.current-branch.outputs.branch}} -m "dummy tag" 84 | # echo "TIMESTAMP=$TIMESTAMP" >> $GITHUB_ENV 85 | 86 | # echo "Running script..." 87 | # bash release.sh 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | cmd/devops/devops 17 | plugins/kubernetes/kubernetes 18 | plugins/helm/helm 19 | frontend/devops 20 | 21 | *.csv 22 | 23 | dist/ 24 | 25 | # Logs 26 | logs 27 | *.log 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | pnpm-debug.log* 32 | lerna-debug.log* 33 | 34 | node_modules 35 | dist 36 | dist-ssr 37 | *.local 38 | 39 | # Editor directories and files 40 | .vscode/* 41 | !.vscode/extensions.json 42 | .idea 43 | .DS_Store 44 | *.suo 45 | *.ntvs* 46 | *.njsproj 47 | *.sln 48 | *.sw? 49 | 50 | gcs-storage-admin.json -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | builds: 5 | - id: devops-core 6 | env: 7 | - CGO_ENABLED=0 8 | flags: 9 | - -tags=release 10 | main: ./cmd/devops 11 | binary: devops 12 | goos: 13 | - linux 14 | # - windows 15 | - darwin 16 | release: 17 | disable: true 18 | skip_upload: true 19 | 20 | archives: 21 | - builds: 22 | - devops-core 23 | id: devops-core 24 | replacements: 25 | # darwin: Darwin 26 | # linux: Linux 27 | # windows: Windows 28 | 386: i386 29 | amd64: x86_64 30 | files: 31 | - ./cmd/devops/config.yaml 32 | checksum: 33 | name_template: "checksums.txt" 34 | snapshot: 35 | name_template: "{{ incpatch .Version }}-next" 36 | changelog: 37 | sort: asc 38 | filters: 39 | exclude: 40 | - "^docs:" 41 | - "^test:" 42 | 43 | blobs: 44 | - provider: gs 45 | bucket: devops-cli-artifacts 46 | folder: "releases/devops/{{.Version}}" 47 | ids: 48 | - devops-core 49 | extra_files: 50 | - glob: ./install.sh 51 | - glob: devops-frontend/ui.tar.gz 52 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Server", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${workspaceFolder}/cmd/devops", 13 | }, 14 | { 15 | "name": "Launch TUI", 16 | "type": "go", 17 | "request": "launch", 18 | "mode": "auto", 19 | "program": "${workspaceFolder}/cmd/devops", 20 | "args": ["tui"] 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Master 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": { 3 | "id": "user-EglFKygBMOzhA0rEAohpLK6R", 4 | "name": "Sharad Regoti", 5 | "email": "sharadregoti15@gmail.com", 6 | "image": "https://lh3.googleusercontent.com/a/AEdFTp7wZy2QTU_Takie4kydPyzpfU37HfKvhsKJIFvKDQ=s96-c", 7 | "picture": "https://lh3.googleusercontent.com/a/AEdFTp7wZy2QTU_Takie4kydPyzpfU37HfKvhsKJIFvKDQ=s96-c", 8 | "groups": [] 9 | }, 10 | "expires": "2023-03-16T18:04:29.649Z", 11 | "accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik1UaEVOVUpHTkVNMVFURTRNMEZCTWpkQ05UZzVNRFUxUlRVd1FVSkRNRU13UmtGRVFrRXpSZyJ9.eyJodHRwczovL2FwaS5vcGVuYWkuY29tL3Byb2ZpbGUiOnsiZW1haWwiOiJzaGFyYWRyZWdvdGkxNUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZ2VvaXBfY291bnRyeSI6IklOIn0sImh0dHBzOi8vYXBpLm9wZW5haS5jb20vYXV0aCI6eyJ1c2VyX2lkIjoidXNlci1FZ2xGS3lnQk1PemhBMHJFQW9ocExLNlIifSwiaXNzIjoiaHR0cHM6Ly9hdXRoMC5vcGVuYWkuY29tLyIsInN1YiI6Imdvb2dsZS1vYXV0aDJ8MTExOTEwMTAwMTM5OTI3NzEyNDY1IiwiYXVkIjpbImh0dHBzOi8vYXBpLm9wZW5haS5jb20vdjEiLCJodHRwczovL29wZW5haS5vcGVuYWkuYXV0aDBhcHAuY29tL3VzZXJpbmZvIl0sImlhdCI6MTY3NjIxMjgxMSwiZXhwIjoxNjc3NDIyNDExLCJhenAiOiJUZEpJY2JlMTZXb1RIdE45NW55eXdoNUU0eU9vNkl0RyIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwgbW9kZWwucmVhZCBtb2RlbC5yZXF1ZXN0IG9yZ2FuaXphdGlvbi5yZWFkIG9mZmxpbmVfYWNjZXNzIn0.daT-sZQRO833xTbxXee1Y3eCaY0KgUOOZn7nZ3HFBvKSVR3YzrvH6P9gZegC2CTMUjt4FPknWsWDUDFfY1gh2wfZ4yPKhp_NVEXJdqbtP0bP94DX-CGI74VwIrlWaCqrpSf5W-3bIe_Bn4fAc09CEnRLVxcTt4WVmgcgVc2iKuwh7Ub3qny2Zeyi1LdzNyjeyITIvHsgxOi3YxX8B69bETDHXZDqx4f0vsnz5AbjS4jWTPRAu9LvKNaqPtPH_5Z68iKHQ0PCYq5uf-09KAeYbJ8zB3HSbFq4ozJs563LZDFi8gbzbLlvxAYOKV9TjxMLNHA5CDPGeb8rpvZ8IhZ4eQ" 12 | } -------------------------------------------------------------------------------- /cmd/devops/config.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | address: ":9753" 3 | plugins: 4 | - name: kubernetes 5 | isDefault: true 6 | - name: helm 7 | -------------------------------------------------------------------------------- /cmd/devops/semantic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // isGreater returns true if v1 is greater than v2 according to semantic versioning rules. 10 | func isGreater(v1, v2 string) bool { 11 | // parse the version numbers 12 | major1, minor1, patch1, err := parseVersion(v1) 13 | if err != nil { 14 | return false 15 | } 16 | major2, minor2, patch2, err := parseVersion(v2) 17 | if err != nil { 18 | // handle error 19 | return false 20 | } 21 | 22 | // compare the versions 23 | if major1 > major2 { 24 | return true 25 | } 26 | if major1 < major2 { 27 | return false 28 | } 29 | if minor1 > minor2 { 30 | return true 31 | } 32 | if minor1 < minor2 { 33 | return false 34 | } 35 | if patch1 > patch2 { 36 | return true 37 | } 38 | return false 39 | } 40 | 41 | // parseVersion parses a semantic version number and returns its components as integers. 42 | func parseVersion(v string) (int, int, int, error) { 43 | parts := strings.Split(v, ".") 44 | if len(parts) != 3 { 45 | return 0, 0, 0, fmt.Errorf("invalid version number: %s", v) 46 | } 47 | 48 | major, err := strconv.Atoi(parts[0]) 49 | if err != nil { 50 | return 0, 0, 0, fmt.Errorf("invalid major version: %s", parts[0]) 51 | } 52 | minor, err := strconv.Atoi(parts[1]) 53 | if err != nil { 54 | return 0, 0, 0, fmt.Errorf("invalid minor version: %s", parts[1]) 55 | } 56 | patch, err := strconv.Atoi(parts[2]) 57 | if err != nil { 58 | return 0, 0, 0, fmt.Errorf("invalid patch version: %s", parts[2]) 59 | } 60 | 61 | return major, minor, patch, nil 62 | } 63 | -------------------------------------------------------------------------------- /commands.sh: -------------------------------------------------------------------------------- 1 | # Generating swagger docs 2 | # From root package 3 | swag init -g cmd/devops/main.go --parseDependency 4 | 5 | 6 | step 1: Increment version number in code 7 | step 2: close all milestone & related issues mentioned in it 8 | step 3: release all binaries 9 | # git tag -a v0.2.0 -m "m" #create dummy tag 10 | # Execute this from the root directory where .releaser file exists 11 | # goreleaser release --rm-dist --skip-validate (Run this for core binary as well as for k8s plugin from that directory) 12 | step 4: Create a new release on devops-cli repository, by mentioning the milestone 13 | step 5: Delete issues from github project 14 | -------------------------------------------------------------------------------- /common/conn_init.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | 6 | "cloud.google.com/go/firestore" 7 | "cloud.google.com/go/logging" 8 | "google.golang.org/api/option" 9 | 10 | "log" 11 | ) 12 | 13 | var logger *log.Logger 14 | var Release = false 15 | 16 | var data = ` 17 | { 18 | "client_id": "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com", 19 | "client_secret": "d-FL95Q19q7MQmFpd7hHD0Ty", 20 | "quota_project_id": "try-out-gcp-features", 21 | "refresh_token": "1//0gEi9-8AIjfiVCgYIARAAGBASNwF-L9Ir0stzEqkcB-y0MLsvg9DoBW_8o2fzXeYF9a5Zir-1VL9QXz-vjZiH89OsQ2kcPrdBdSs", 22 | "type": "authorized_user" 23 | }` 24 | 25 | func ConnInit() { 26 | 27 | ctx := context.Background() 28 | 29 | // Creates a client. 30 | client, err := logging.NewClient(ctx, projectID, option.WithCredentialsJSON([]byte(data))) 31 | if err != nil { 32 | log.Printf("Failed to create client: %v", err) 33 | } 34 | // defer client.Close() 35 | 36 | // Sets the name of the log to write to. 37 | logName := "devops-cli" 38 | 39 | logger = client.Logger(logName).StandardLogger(logging.Error) 40 | 41 | clientm, err := firestore.NewClient(ctx, projectID, option.WithCredentialsJSON([]byte(data))) 42 | if err != nil { 43 | log.Printf("Failed to create client: %v", err) 44 | } 45 | 46 | fClient = clientm 47 | } 48 | 49 | func ConnLoggingInit() { 50 | ctx := context.Background() 51 | 52 | // Creates a client. 53 | client, err := logging.NewClient(ctx, projectID, option.WithCredentialsJSON([]byte(data))) 54 | if err != nil { 55 | log.Printf("Failed to create client: %v", err) 56 | } 57 | // defer client.Close() 58 | 59 | // Sets the name of the log to write to. 60 | logName := "devops-cli" 61 | 62 | logger = client.Logger(logName).StandardLogger(logging.Error) 63 | } 64 | -------------------------------------------------------------------------------- /common/monitoring.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "time" 9 | 10 | "cloud.google.com/go/firestore" 11 | ) 12 | 13 | var fClient *firestore.Client 14 | var projectID = "try-out-gcp-features" 15 | 16 | func IncrementAppStarts() error { 17 | if fClient == nil { 18 | return nil 19 | } 20 | 21 | ctx := context.Background() 22 | docRef := fClient.Collection("metrics").NewDoc() 23 | user := map[string]interface{}{ 24 | "type": "start_counter", 25 | "labels": getUniqueInfo(), 26 | "value": 1, 27 | } 28 | 29 | if _, err := docRef.Set(ctx, user); err != nil { 30 | log.Printf("Failed to set data: %v", err) 31 | } 32 | return nil 33 | } 34 | 35 | func getUniqueInfo() map[string]string { 36 | hostname, err := os.Hostname() 37 | if err != nil { 38 | fmt.Println(err) 39 | return map[string]string{} 40 | } 41 | 42 | homeDir, err := os.UserHomeDir() 43 | if err != nil { 44 | fmt.Println(err) 45 | return map[string]string{} 46 | } 47 | return map[string]string{ 48 | "home": homeDir, 49 | "host": hostname, 50 | } 51 | } 52 | 53 | func ReportUsageTime(startTime, endTime time.Time) error { 54 | if fClient == nil { 55 | return nil 56 | } 57 | 58 | // Calculate the usage time 59 | usageTime := endTime.Sub(startTime) 60 | 61 | ctx := context.Background() 62 | docRef := fClient.Collection("metrics").NewDoc() 63 | user := map[string]interface{}{ 64 | "type": "usage_time", 65 | "labels": getUniqueInfo(), 66 | "value": usageTime.Seconds(), 67 | } 68 | 69 | if _, err := docRef.Set(ctx, user); err != nil { 70 | log.Printf("Failed to set data: %v", err) 71 | } 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /common/release.go: -------------------------------------------------------------------------------- 1 | //go:build release 2 | // +build release 3 | 4 | package common 5 | 6 | import ( 7 | // "cloud.google.com/go/firestore" 8 | // "cloud.google.com/go/logging" 9 | // "context" 10 | // "google.golang.org/api/option" 11 | 12 | // "log" 13 | ) 14 | 15 | func init() { 16 | Release = true 17 | } 18 | -------------------------------------------------------------------------------- /devops-frontend/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:8080", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /devops-frontend/README.md: -------------------------------------------------------------------------------- 1 | # devops-frontend 2 | 3 | ## Install Dependencies 4 | 5 | `yarn` 6 | 7 | ## Run in local 8 | 9 | `yarn dev` 10 | -------------------------------------------------------------------------------- /devops-frontend/commands.sh: -------------------------------------------------------------------------------- 1 | # Command to generate initial code from swagger.yaml 2 | openapi-generator-cli generate -i ./swagger.yaml -o src/generated-sources/openapi -g typescript-fetch --additional-properties=supportsES6=true,npmVersion=6.9.0,typescriptThreePlus=true 3 | 4 | # For building the project 5 | # Uncomment the production URL 6 | vite build 7 | tar -czvf ui.tar.gz dist/ 8 | cp ui.tar.gz ~/.devops/ 9 | tar -xvzf ui.tar.gz -------------------------------------------------------------------------------- /devops-frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /devops-frontend/openapitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", 3 | "spaces": 2, 4 | "generator-cli": { 5 | "version": "6.4.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /devops-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devops-frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "codegen": "rm -rf src/generated-sources/openapi; openapi-generator-cli generate -i ./json-placeholder-api.yaml -o src/generated-sources/openapi -g typescript-fetch --additional-properties=supportsES6=true,npmVersion=6.9.0,typescriptThreePlus=true" 11 | }, 12 | "dependencies": { 13 | "@monaco-editor/react": "^4.4.6", 14 | "@reduxjs/toolkit": "^1.9.3", 15 | "@types/react-router-dom": "^5.3.3", 16 | "antd": "^5.1.6", 17 | "axios": "^1.2.3", 18 | "fuse.js": "^6.6.2", 19 | "js-yaml": "^4.1.0", 20 | "monaco-editor": "^0.36.1", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-fuzzy": "^1.3.0", 24 | "react-redux": "^8.0.5", 25 | "react-router-dom": "^6.10.0", 26 | "react-use-websocket": "^4.3.1", 27 | "redux": "^4.2.0", 28 | "redux-thunk": "^2.4.2", 29 | "xterm": "^5.1.0", 30 | "xterm-addon-attach": "^0.8.0", 31 | "xterm-addon-fit": "^0.7.0", 32 | "xterm-for-react": "^1.0.4", 33 | "yaml": "^2.2.1", 34 | "yamljs": "^0.3.0" 35 | }, 36 | "devDependencies": { 37 | "@types/react": "^18.0.26", 38 | "@types/react-dom": "^18.0.9", 39 | "@types/react-redux": "^7.1.25", 40 | "@types/redux": "^3.6.0", 41 | "@types/redux-thunk": "^2.1.0", 42 | "@vitejs/plugin-react": "^3.0.0", 43 | "typescript": "^4.9.3", 44 | "vite": "^4.0.0" 45 | } 46 | } -------------------------------------------------------------------------------- /devops-frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /devops-frontend/src/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharadregoti/devops-cli/81b5f40238d73abc985cbbe82e80b10419bf4b2b/devops-frontend/src/App.css -------------------------------------------------------------------------------- /devops-frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import Home from './pages/Home' 2 | import PluginSelector from './pages/PluginSelector' 3 | import { ConfigProvider, theme } from 'antd' 4 | import './App.css' 5 | import { BrowserRouter as Router, Route, Routes, useParams } from 'react-router-dom'; 6 | import { Provider } from 'react-redux'; 7 | import store from './redux/store'; 8 | 9 | function App() { 10 | return ( 11 | 16 | 17 | 18 | 19 | } /> 20 | } /> 21 | {/* } /> 22 | } /> */} 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | export default App 31 | -------------------------------------------------------------------------------- /devops-frontend/src/assets/helmicon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharadregoti/devops-cli/81b5f40238d73abc985cbbe82e80b10419bf4b2b/devops-frontend/src/assets/helmicon-32.png -------------------------------------------------------------------------------- /devops-frontend/src/assets/helmicon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharadregoti/devops-cli/81b5f40238d73abc985cbbe82e80b10419bf4b2b/devops-frontend/src/assets/helmicon-64.png -------------------------------------------------------------------------------- /devops-frontend/src/assets/kubernetes32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharadregoti/devops-cli/81b5f40238d73abc985cbbe82e80b10419bf4b2b/devops-frontend/src/assets/kubernetes32.png -------------------------------------------------------------------------------- /devops-frontend/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /devops-frontend/src/components/drawer/Drawer.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharadregoti/devops-cli/81b5f40238d73abc985cbbe82e80b10419bf4b2b/devops-frontend/src/components/drawer/Drawer.css -------------------------------------------------------------------------------- /devops-frontend/src/components/drawer/Drawer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import { Drawer, Spin, Button } from 'antd'; 3 | import XTermComponent from '../xTerm/XTerm'; 4 | import Editor from '@monaco-editor/react'; 5 | import { showNotification } from '../../utils/notification'; 6 | import { api } from '../../utils/config'; 7 | import { AppState } from '../../types/Event' 8 | import { ModelFrontendEvent, ModelFrontendEventNameEnum, HandleEventRequest } from '../../generated-sources/openapi'; 9 | import yaml from "yamljs"; 10 | 11 | type DrawerBodyType = "editor" | "xterm" | "table"; 12 | 13 | export type DrawerPropsTypes = { 14 | socketUrl: string 15 | drawerBodyType: DrawerBodyType 16 | isDrawerOpen: boolean 17 | resourceName: string 18 | appConfig: AppState 19 | editorOptions: EditorOptions 20 | onDrawerClose: () => void 21 | } 22 | 23 | type EditorOptions = { 24 | isReadOnly: boolean 25 | defaultText: string 26 | } 27 | 28 | const SideDrawer: React.FC = ({ isDrawerOpen, socketUrl, drawerBodyType, resourceName, appConfig, editorOptions, onDrawerClose }) => { 29 | console.log('Rendering SideDrawer...'); 30 | 31 | const terminalRef = useRef(null); 32 | const editorRef = useRef(null); 33 | 34 | // const [drawerLoading, setDrawerLoading] = useState(false); 35 | 36 | const handleEditorSaveButton = () => { 37 | // TODO: Handle spinner 38 | // setDrawerLoading(true); 39 | 40 | // Get the current content of the editor 41 | const content = editorRef.current.getValue(); 42 | 43 | let yamlobj = {} 44 | try { 45 | yamlobj = yaml.parse(content); 46 | } catch (error) { 47 | // TODO: Handle spinner 48 | // setDrawerLoading(false); 49 | showNotification('error', 'Invalid YAML', error.message) 50 | return 51 | } 52 | 53 | const e: ModelFrontendEvent = { 54 | eventType: "normal-action", 55 | name: ModelFrontendEventNameEnum.Edit, 56 | isolatorName: appConfig?.currentIsolator, 57 | pluginName: appConfig?.currentPluginName, 58 | resourceName: resourceName, 59 | resourceType: appConfig?.currentResourceType, 60 | args: yamlobj, 61 | } 62 | 63 | let params: HandleEventRequest = { 64 | id: appConfig.generalInfo.id, 65 | modelFrontendEvent: e 66 | } 67 | 68 | api.handleEvent(params) 69 | .then(res => { 70 | // setDrawerLoading(false); 71 | // setOpen(false); 72 | onDrawerClose() 73 | showNotification('success', `Successfully updated ${e.resourceType}`, '') 74 | }) 75 | .catch(err => { 76 | // setDrawerLoading(false); 77 | showNotification('error', 'Event invocation failed', err) 78 | }) 79 | } 80 | 81 | return ( 82 | 89 | {/* TODO: Handle spinner */} 90 | {/* */} 91 | {drawerBodyType == 'editor' && <> 92 | { 98 | // Store a reference to the editor instance 99 | editorRef.current = editor; 100 | }} 101 | /> 102 | {!editorOptions.isReadOnly && 103 | 104 | } 105 | } 106 | {drawerBodyType == 'xterm' && 107 | 108 | } 109 | {/* */} 110 | 111 | ); 112 | }; 113 | 114 | export default SideDrawer; -------------------------------------------------------------------------------- /devops-frontend/src/components/infoCard/InfoCard.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharadregoti/devops-cli/81b5f40238d73abc985cbbe82e80b10419bf4b2b/devops-frontend/src/components/infoCard/InfoCard.css -------------------------------------------------------------------------------- /devops-frontend/src/components/infoCard/InfoCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Card, Descriptions, Typography } from "antd"; 3 | import { Action, General, Plugins } from "../../types/InfoCardTypes"; 4 | import './InfoCard.css' 5 | 6 | type InfoCardPropsTypes = { 7 | title: string; 8 | content: Action | Plugins | General | { 0: string } | {}; 9 | } 10 | 11 | const InfoCard: React.FC = ({ title, content }) => { 12 | const keyValueStyle = { 13 | display: "block", 14 | maxWidth: "40ch", 15 | whiteSpace: "nowrap", 16 | overflow: "hidden", 17 | textOverflow: "ellipsis", 18 | lineHeight: "1.5", 19 | paddingRight: "1ch", 20 | // display: "block", 21 | }; 22 | 23 | return ( 24 | <> 25 | 29 | {content && 30 | Object.entries(content).map(([key, value]: [string, string]) => ( 31 | // Object.entries(content).slice(0, 4).map(([key, value]: [string, string]) => ( 32 | 33 | {/* {key}: {value} */} 34 | {value} 35 | 36 | ))} 37 | 38 | 39 | ); 40 | } 41 | 42 | 43 | export default InfoCard; -------------------------------------------------------------------------------- /devops-frontend/src/components/isolator/Isolator.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Card, Typography, Select, Radio, Space, Row, Col } from "antd"; 3 | import type { RadioChangeEvent } from 'antd'; 4 | import { AppState } from '../../types/Event' 5 | 6 | // import './InfoCard.css' 7 | import Paragraph from "antd/es/skeleton/Paragraph"; 8 | 9 | export type InfoCardPropsTypes = { 10 | currentIsolator: string, 11 | defaultIsolator: string, 12 | isolators: string[], 13 | frequentlyUsed: string[], 14 | appConfig: AppState 15 | onNamespaceChange: (isolatorName: string) => void 16 | } 17 | 18 | const IsolatorCard: React.FC = ({ currentIsolator, defaultIsolator, isolators, frequentlyUsed, appConfig, onNamespaceChange }) => { 19 | const onChange = (e: RadioChangeEvent) => { 20 | onNamespaceChange(e.target.value) 21 | }; 22 | 23 | const [selectWidth, setSelectWidth] = useState() 24 | 25 | function getTextWidth(text, font) { 26 | const canvas = document.createElement('canvas'); 27 | const context = canvas.getContext('2d'); 28 | context.font = font; 29 | const metrics = context.measureText(text); 30 | return metrics.width; 31 | } 32 | 33 | useEffect(() => { 34 | if (isolators.length === 0) { 35 | return 36 | } 37 | 38 | const longestIsolatorName = isolators 39 | ? isolators.reduce((longest, isolator) => { 40 | return isolator.length > longest.length ? isolator : longest; 41 | }, '') 42 | : ''; 43 | 44 | const selectWidth = getTextWidth(longestIsolatorName, '16px Arial') + 50; // 50px extra for padding and dropdown arrow 45 | setSelectWidth(selectWidth); 46 | }, [isolators]) 47 | 48 | return ( 49 | <> 50 | 51 | 52 | Isolator 53 | 54 | 55 | 74 | 75 | ) 76 | }) 77 | } 78 | 79 | 80 | ); 81 | }; 82 | 83 | export default SpecificActionForm; -------------------------------------------------------------------------------- /devops-frontend/src/components/xTerm/XTerm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useLayoutEffect } from 'react'; 2 | import { Terminal } from 'xterm'; 3 | import { AttachAddon } from 'xterm-addon-attach'; 4 | import { FitAddon } from 'xterm-addon-fit'; 5 | 6 | type XTermComponentPropsTypes = { 7 | socketUrl: string, 8 | isDrawerOpen: boolean 9 | } 10 | 11 | const XTermComponent: React.FC = ({ isDrawerOpen, socketUrl }) => { 12 | const containerRef = useRef(null); 13 | const terminalRef = useRef(null); 14 | 15 | const fitAddonRef = useRef(null); 16 | 17 | console.log("MOunt XTermComponent"); 18 | 19 | useLayoutEffect(() => { 20 | console.log("isDrawerOpen: ", isDrawerOpen); 21 | 22 | if (isDrawerOpen) { 23 | // Create a new Terminal instance 24 | terminalRef.current = new Terminal(); 25 | 26 | // Attach the terminal to the container element 27 | terminalRef.current.open(containerRef.current); 28 | 29 | // Connect to the WebSocket and attach it to the terminal 30 | const socket = new WebSocket(socketUrl); 31 | const attachAddon = new AttachAddon(socket); 32 | const fitAddon = new FitAddon(); 33 | terminalRef.current.loadAddon(attachAddon); 34 | terminalRef.current.loadAddon(fitAddon); 35 | 36 | // Delay the fitAddon.fit() call with setTimeout 37 | setTimeout(() => { 38 | fitAddon.fit(); 39 | console.log('Fitting terminal'); 40 | }, 500); 41 | 42 | // fitAddon.fit(); 43 | // console.log("Fitting terminal"); 44 | 45 | // Store the fitAddon instance in a ref so it can be accessed in the cleanup function 46 | fitAddonRef.current = fitAddon; 47 | 48 | // Add any additional logic here... 49 | } else { 50 | console.log("Closing terminal"); 51 | // Clean up the terminal when the drawer is closed 52 | // terminalRef.current?.dispose(); 53 | terminalRef.current?.dispose(); 54 | fitAddonRef.current?.dispose(); 55 | } 56 | 57 | // TODO: Close websocket connection 58 | // Return a cleanup function to dispose of the FitAddon instance 59 | return () => { 60 | console.log("Unmount XTermComponent"); 61 | // terminalRef.current?.dispose(); 62 | // fitAddonRef.current?.dispose(); 63 | }; 64 | }, [isDrawerOpen, socketUrl]); 65 | 66 | return
; 67 | }; 68 | 69 | export default XTermComponent; -------------------------------------------------------------------------------- /devops-frontend/src/generated-sources/openapi/.openapi-generator-ignore: -------------------------------------------------------------------------------- 1 | # OpenAPI Generator Ignore 2 | # Generated by openapi-generator https://github.com/openapitools/openapi-generator 3 | 4 | # Use this file to prevent files from being overwritten by the generator. 5 | # The patterns follow closely to .gitignore or .dockerignore. 6 | 7 | # As an example, the C# client generator defines ApiClient.cs. 8 | # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: 9 | #ApiClient.cs 10 | 11 | # You can match any string of characters against a directory, file or extension with a single asterisk (*): 12 | #foo/*/qux 13 | # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux 14 | 15 | # You can recursively match patterns against a directory, file or extension with a double asterisk (**): 16 | #foo/**/qux 17 | # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux 18 | 19 | # You can also negate patterns with an exclamation (!). 20 | # For example, you can ignore all files in a docs folder with the file extension .md: 21 | #docs/*.md 22 | # Then explicitly reverse the ignore rule for a single file: 23 | #!docs/README.md 24 | -------------------------------------------------------------------------------- /devops-frontend/src/generated-sources/openapi/.openapi-generator/FILES: -------------------------------------------------------------------------------- 1 | apis/DefaultApi.ts 2 | apis/index.ts 3 | index.ts 4 | models/ModelAuthResponse.ts 5 | models/ModelConfig.ts 6 | models/ModelErrorResponse.ts 7 | models/ModelEventResponse.ts 8 | models/ModelFrontendEvent.ts 9 | models/ModelInfoResponse.ts 10 | models/ModelPlugin.ts 11 | models/ModelServer.ts 12 | models/ProtoAction.ts 13 | models/ProtoAuthInfo.ts 14 | models/ProtoExecution.ts 15 | models/ProtoServerInput.ts 16 | models/ProtoUserInput.ts 17 | models/StructpbValue.ts 18 | models/index.ts 19 | runtime.ts 20 | -------------------------------------------------------------------------------- /devops-frontend/src/generated-sources/openapi/.openapi-generator/VERSION: -------------------------------------------------------------------------------- 1 | 6.4.0 -------------------------------------------------------------------------------- /devops-frontend/src/generated-sources/openapi/apis/index.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export * from './DefaultApi'; 4 | -------------------------------------------------------------------------------- /devops-frontend/src/generated-sources/openapi/index.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export * from './runtime'; 4 | export * from './apis'; 5 | export * from './models'; 6 | -------------------------------------------------------------------------------- /devops-frontend/src/generated-sources/openapi/models/ModelAuthResponse.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * NEW Devops API 5 | * Devops API Sec 6 | * 7 | * The version of the OpenAPI document: v0.1.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import { exists, mapValues } from '../runtime'; 16 | import type { ProtoAuthInfo } from './ProtoAuthInfo'; 17 | import { 18 | ProtoAuthInfoFromJSON, 19 | ProtoAuthInfoFromJSONTyped, 20 | ProtoAuthInfoToJSON, 21 | } from './ProtoAuthInfo'; 22 | 23 | /** 24 | * 25 | * @export 26 | * @interface ModelAuthResponse 27 | */ 28 | export interface ModelAuthResponse { 29 | /** 30 | * 31 | * @type {Array} 32 | * @memberof ModelAuthResponse 33 | */ 34 | auths?: Array; 35 | } 36 | 37 | /** 38 | * Check if a given object implements the ModelAuthResponse interface. 39 | */ 40 | export function instanceOfModelAuthResponse(value: object): boolean { 41 | let isInstance = true; 42 | 43 | return isInstance; 44 | } 45 | 46 | export function ModelAuthResponseFromJSON(json: any): ModelAuthResponse { 47 | return ModelAuthResponseFromJSONTyped(json, false); 48 | } 49 | 50 | export function ModelAuthResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): ModelAuthResponse { 51 | if ((json === undefined) || (json === null)) { 52 | return json; 53 | } 54 | return { 55 | 56 | 'auths': !exists(json, 'auths') ? undefined : ((json['auths'] as Array).map(ProtoAuthInfoFromJSON)), 57 | }; 58 | } 59 | 60 | export function ModelAuthResponseToJSON(value?: ModelAuthResponse | null): any { 61 | if (value === undefined) { 62 | return undefined; 63 | } 64 | if (value === null) { 65 | return null; 66 | } 67 | return { 68 | 69 | 'auths': value.auths === undefined ? undefined : ((value.auths as Array).map(ProtoAuthInfoToJSON)), 70 | }; 71 | } 72 | 73 | -------------------------------------------------------------------------------- /devops-frontend/src/generated-sources/openapi/models/ModelConfig.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * NEW Devops API 5 | * Devops API Sec 6 | * 7 | * The version of the OpenAPI document: v0.1.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import { exists, mapValues } from '../runtime'; 16 | import type { ModelPlugin } from './ModelPlugin'; 17 | import { 18 | ModelPluginFromJSON, 19 | ModelPluginFromJSONTyped, 20 | ModelPluginToJSON, 21 | } from './ModelPlugin'; 22 | import type { ModelServer } from './ModelServer'; 23 | import { 24 | ModelServerFromJSON, 25 | ModelServerFromJSONTyped, 26 | ModelServerToJSON, 27 | } from './ModelServer'; 28 | 29 | /** 30 | * 31 | * @export 32 | * @interface ModelConfig 33 | */ 34 | export interface ModelConfig { 35 | /** 36 | * 37 | * @type {Array} 38 | * @memberof ModelConfig 39 | */ 40 | plugins: Array; 41 | /** 42 | * 43 | * @type {ModelServer} 44 | * @memberof ModelConfig 45 | */ 46 | server: ModelServer; 47 | } 48 | 49 | /** 50 | * Check if a given object implements the ModelConfig interface. 51 | */ 52 | export function instanceOfModelConfig(value: object): boolean { 53 | let isInstance = true; 54 | isInstance = isInstance && "plugins" in value; 55 | isInstance = isInstance && "server" in value; 56 | 57 | return isInstance; 58 | } 59 | 60 | export function ModelConfigFromJSON(json: any): ModelConfig { 61 | return ModelConfigFromJSONTyped(json, false); 62 | } 63 | 64 | export function ModelConfigFromJSONTyped(json: any, ignoreDiscriminator: boolean): ModelConfig { 65 | if ((json === undefined) || (json === null)) { 66 | return json; 67 | } 68 | return { 69 | 70 | 'plugins': ((json['plugins'] as Array).map(ModelPluginFromJSON)), 71 | 'server': ModelServerFromJSON(json['server']), 72 | }; 73 | } 74 | 75 | export function ModelConfigToJSON(value?: ModelConfig | null): any { 76 | if (value === undefined) { 77 | return undefined; 78 | } 79 | if (value === null) { 80 | return null; 81 | } 82 | return { 83 | 84 | 'plugins': ((value.plugins as Array).map(ModelPluginToJSON)), 85 | 'server': ModelServerToJSON(value.server), 86 | }; 87 | } 88 | 89 | -------------------------------------------------------------------------------- /devops-frontend/src/generated-sources/openapi/models/ModelErrorResponse.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * NEW Devops API 5 | * Devops API Sec 6 | * 7 | * The version of the OpenAPI document: v0.1.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import { exists, mapValues } from '../runtime'; 16 | /** 17 | * 18 | * @export 19 | * @interface ModelErrorResponse 20 | */ 21 | export interface ModelErrorResponse { 22 | /** 23 | * 24 | * @type {string} 25 | * @memberof ModelErrorResponse 26 | */ 27 | message?: string; 28 | } 29 | 30 | /** 31 | * Check if a given object implements the ModelErrorResponse interface. 32 | */ 33 | export function instanceOfModelErrorResponse(value: object): boolean { 34 | let isInstance = true; 35 | 36 | return isInstance; 37 | } 38 | 39 | export function ModelErrorResponseFromJSON(json: any): ModelErrorResponse { 40 | return ModelErrorResponseFromJSONTyped(json, false); 41 | } 42 | 43 | export function ModelErrorResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): ModelErrorResponse { 44 | if ((json === undefined) || (json === null)) { 45 | return json; 46 | } 47 | return { 48 | 49 | 'message': !exists(json, 'message') ? undefined : json['message'], 50 | }; 51 | } 52 | 53 | export function ModelErrorResponseToJSON(value?: ModelErrorResponse | null): any { 54 | if (value === undefined) { 55 | return undefined; 56 | } 57 | if (value === null) { 58 | return null; 59 | } 60 | return { 61 | 62 | 'message': value.message, 63 | }; 64 | } 65 | 66 | -------------------------------------------------------------------------------- /devops-frontend/src/generated-sources/openapi/models/ModelEventResponse.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * NEW Devops API 5 | * Devops API Sec 6 | * 7 | * The version of the OpenAPI document: v0.1.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import { exists, mapValues } from '../runtime'; 16 | /** 17 | * 18 | * @export 19 | * @interface ModelEventResponse 20 | */ 21 | export interface ModelEventResponse { 22 | /** 23 | * 24 | * @type {string} 25 | * @memberof ModelEventResponse 26 | */ 27 | id?: string; 28 | /** 29 | * 30 | * @type {object} 31 | * @memberof ModelEventResponse 32 | */ 33 | result?: object; 34 | } 35 | 36 | /** 37 | * Check if a given object implements the ModelEventResponse interface. 38 | */ 39 | export function instanceOfModelEventResponse(value: object): boolean { 40 | let isInstance = true; 41 | 42 | return isInstance; 43 | } 44 | 45 | export function ModelEventResponseFromJSON(json: any): ModelEventResponse { 46 | return ModelEventResponseFromJSONTyped(json, false); 47 | } 48 | 49 | export function ModelEventResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): ModelEventResponse { 50 | if ((json === undefined) || (json === null)) { 51 | return json; 52 | } 53 | return { 54 | 55 | 'id': !exists(json, 'id') ? undefined : json['id'], 56 | 'result': !exists(json, 'result') ? undefined : json['result'], 57 | }; 58 | } 59 | 60 | export function ModelEventResponseToJSON(value?: ModelEventResponse | null): any { 61 | if (value === undefined) { 62 | return undefined; 63 | } 64 | if (value === null) { 65 | return null; 66 | } 67 | return { 68 | 69 | 'id': value.id, 70 | 'result': value.result, 71 | }; 72 | } 73 | 74 | -------------------------------------------------------------------------------- /devops-frontend/src/generated-sources/openapi/models/ModelFrontendEvent.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * NEW Devops API 5 | * Devops API Sec 6 | * 7 | * The version of the OpenAPI document: v0.1.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import { exists, mapValues } from '../runtime'; 16 | /** 17 | * 18 | * @export 19 | * @interface ModelFrontendEvent 20 | */ 21 | export interface ModelFrontendEvent { 22 | /** 23 | * 24 | * @type {object} 25 | * @memberof ModelFrontendEvent 26 | */ 27 | args?: object; 28 | /** 29 | * 30 | * @type {string} 31 | * @memberof ModelFrontendEvent 32 | */ 33 | eventType?: string; 34 | /** 35 | * 36 | * @type {string} 37 | * @memberof ModelFrontendEvent 38 | */ 39 | isolatorName?: string; 40 | /** 41 | * 42 | * @type {string} 43 | * @memberof ModelFrontendEvent 44 | */ 45 | name?: ModelFrontendEventNameEnum; 46 | /** 47 | * 48 | * @type {string} 49 | * @memberof ModelFrontendEvent 50 | */ 51 | pluginName?: string; 52 | /** 53 | * 54 | * @type {string} 55 | * @memberof ModelFrontendEvent 56 | */ 57 | resourceName?: string; 58 | /** 59 | * 60 | * @type {string} 61 | * @memberof ModelFrontendEvent 62 | */ 63 | resourceType?: string; 64 | } 65 | 66 | 67 | /** 68 | * @export 69 | */ 70 | export const ModelFrontendEventNameEnum = { 71 | Read: 'read', 72 | Delete: 'delete', 73 | Update: 'update', 74 | Create: 'create', 75 | Edit: 'edit', 76 | ViewLongRunning: 'view-long-running', 77 | DeleteLongRunning: 'delete-long-running', 78 | ResourceTypeChange: 'resource-type-change', 79 | IsolatorChange: 'isolator-change', 80 | RefreshResource: 'refresh-resource' 81 | } as const; 82 | export type ModelFrontendEventNameEnum = typeof ModelFrontendEventNameEnum[keyof typeof ModelFrontendEventNameEnum]; 83 | 84 | 85 | /** 86 | * Check if a given object implements the ModelFrontendEvent interface. 87 | */ 88 | export function instanceOfModelFrontendEvent(value: object): boolean { 89 | let isInstance = true; 90 | 91 | return isInstance; 92 | } 93 | 94 | export function ModelFrontendEventFromJSON(json: any): ModelFrontendEvent { 95 | return ModelFrontendEventFromJSONTyped(json, false); 96 | } 97 | 98 | export function ModelFrontendEventFromJSONTyped(json: any, ignoreDiscriminator: boolean): ModelFrontendEvent { 99 | if ((json === undefined) || (json === null)) { 100 | return json; 101 | } 102 | return { 103 | 104 | 'args': !exists(json, 'args') ? undefined : json['args'], 105 | 'eventType': !exists(json, 'eventType') ? undefined : json['eventType'], 106 | 'isolatorName': !exists(json, 'isolatorName') ? undefined : json['isolatorName'], 107 | 'name': !exists(json, 'name') ? undefined : json['name'], 108 | 'pluginName': !exists(json, 'pluginName') ? undefined : json['pluginName'], 109 | 'resourceName': !exists(json, 'resourceName') ? undefined : json['resourceName'], 110 | 'resourceType': !exists(json, 'resourceType') ? undefined : json['resourceType'], 111 | }; 112 | } 113 | 114 | export function ModelFrontendEventToJSON(value?: ModelFrontendEvent | null): any { 115 | if (value === undefined) { 116 | return undefined; 117 | } 118 | if (value === null) { 119 | return null; 120 | } 121 | return { 122 | 123 | 'args': value.args, 124 | 'eventType': value.eventType, 125 | 'isolatorName': value.isolatorName, 126 | 'name': value.name, 127 | 'pluginName': value.pluginName, 128 | 'resourceName': value.resourceName, 129 | 'resourceType': value.resourceType, 130 | }; 131 | } 132 | 133 | -------------------------------------------------------------------------------- /devops-frontend/src/generated-sources/openapi/models/ModelInfoResponse.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * NEW Devops API 5 | * Devops API Sec 6 | * 7 | * The version of the OpenAPI document: v0.1.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import { exists, mapValues } from '../runtime'; 16 | import type { ProtoAction } from './ProtoAction'; 17 | import { 18 | ProtoActionFromJSON, 19 | ProtoActionFromJSONTyped, 20 | ProtoActionToJSON, 21 | } from './ProtoAction'; 22 | 23 | /** 24 | * 25 | * @export 26 | * @interface ModelInfoResponse 27 | */ 28 | export interface ModelInfoResponse { 29 | /** 30 | * 31 | * @type {Array} 32 | * @memberof ModelInfoResponse 33 | */ 34 | actions: Array; 35 | /** 36 | * 37 | * @type {Array} 38 | * @memberof ModelInfoResponse 39 | */ 40 | defaultIsolator: Array; 41 | /** 42 | * 43 | * @type {{ [key: string]: string; }} 44 | * @memberof ModelInfoResponse 45 | */ 46 | general: { [key: string]: string; }; 47 | /** 48 | * 49 | * @type {string} 50 | * @memberof ModelInfoResponse 51 | */ 52 | id: string; 53 | /** 54 | * 55 | * @type {string} 56 | * @memberof ModelInfoResponse 57 | */ 58 | isolatorType: string; 59 | /** 60 | * 61 | * @type {Array} 62 | * @memberof ModelInfoResponse 63 | */ 64 | resourceTypes: Array; 65 | } 66 | 67 | /** 68 | * Check if a given object implements the ModelInfoResponse interface. 69 | */ 70 | export function instanceOfModelInfoResponse(value: object): boolean { 71 | let isInstance = true; 72 | isInstance = isInstance && "actions" in value; 73 | isInstance = isInstance && "defaultIsolator" in value; 74 | isInstance = isInstance && "general" in value; 75 | isInstance = isInstance && "id" in value; 76 | isInstance = isInstance && "isolatorType" in value; 77 | isInstance = isInstance && "resourceTypes" in value; 78 | 79 | return isInstance; 80 | } 81 | 82 | export function ModelInfoResponseFromJSON(json: any): ModelInfoResponse { 83 | return ModelInfoResponseFromJSONTyped(json, false); 84 | } 85 | 86 | export function ModelInfoResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): ModelInfoResponse { 87 | if ((json === undefined) || (json === null)) { 88 | return json; 89 | } 90 | return { 91 | 92 | 'actions': ((json['actions'] as Array).map(ProtoActionFromJSON)), 93 | 'defaultIsolator': json['defaultIsolator'], 94 | 'general': json['general'], 95 | 'id': json['id'], 96 | 'isolatorType': json['isolatorType'], 97 | 'resourceTypes': json['resourceTypes'], 98 | }; 99 | } 100 | 101 | export function ModelInfoResponseToJSON(value?: ModelInfoResponse | null): any { 102 | if (value === undefined) { 103 | return undefined; 104 | } 105 | if (value === null) { 106 | return null; 107 | } 108 | return { 109 | 110 | 'actions': ((value.actions as Array).map(ProtoActionToJSON)), 111 | 'defaultIsolator': value.defaultIsolator, 112 | 'general': value.general, 113 | 'id': value.id, 114 | 'isolatorType': value.isolatorType, 115 | 'resourceTypes': value.resourceTypes, 116 | }; 117 | } 118 | 119 | -------------------------------------------------------------------------------- /devops-frontend/src/generated-sources/openapi/models/ModelPlugin.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * NEW Devops API 5 | * Devops API Sec 6 | * 7 | * The version of the OpenAPI document: v0.1.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import { exists, mapValues } from '../runtime'; 16 | /** 17 | * 18 | * @export 19 | * @interface ModelPlugin 20 | */ 21 | export interface ModelPlugin { 22 | /** 23 | * 24 | * @type {boolean} 25 | * @memberof ModelPlugin 26 | */ 27 | isDefault: boolean; 28 | /** 29 | * 30 | * @type {string} 31 | * @memberof ModelPlugin 32 | */ 33 | name: string; 34 | } 35 | 36 | /** 37 | * Check if a given object implements the ModelPlugin interface. 38 | */ 39 | export function instanceOfModelPlugin(value: object): boolean { 40 | let isInstance = true; 41 | isInstance = isInstance && "isDefault" in value; 42 | isInstance = isInstance && "name" in value; 43 | 44 | return isInstance; 45 | } 46 | 47 | export function ModelPluginFromJSON(json: any): ModelPlugin { 48 | return ModelPluginFromJSONTyped(json, false); 49 | } 50 | 51 | export function ModelPluginFromJSONTyped(json: any, ignoreDiscriminator: boolean): ModelPlugin { 52 | if ((json === undefined) || (json === null)) { 53 | return json; 54 | } 55 | return { 56 | 57 | 'isDefault': json['isDefault'], 58 | 'name': json['name'], 59 | }; 60 | } 61 | 62 | export function ModelPluginToJSON(value?: ModelPlugin | null): any { 63 | if (value === undefined) { 64 | return undefined; 65 | } 66 | if (value === null) { 67 | return null; 68 | } 69 | return { 70 | 71 | 'isDefault': value.isDefault, 72 | 'name': value.name, 73 | }; 74 | } 75 | 76 | -------------------------------------------------------------------------------- /devops-frontend/src/generated-sources/openapi/models/ModelServer.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * NEW Devops API 5 | * Devops API Sec 6 | * 7 | * The version of the OpenAPI document: v0.1.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import { exists, mapValues } from '../runtime'; 16 | /** 17 | * 18 | * @export 19 | * @interface ModelServer 20 | */ 21 | export interface ModelServer { 22 | /** 23 | * 24 | * @type {string} 25 | * @memberof ModelServer 26 | */ 27 | address: string; 28 | } 29 | 30 | /** 31 | * Check if a given object implements the ModelServer interface. 32 | */ 33 | export function instanceOfModelServer(value: object): boolean { 34 | let isInstance = true; 35 | isInstance = isInstance && "address" in value; 36 | 37 | return isInstance; 38 | } 39 | 40 | export function ModelServerFromJSON(json: any): ModelServer { 41 | return ModelServerFromJSONTyped(json, false); 42 | } 43 | 44 | export function ModelServerFromJSONTyped(json: any, ignoreDiscriminator: boolean): ModelServer { 45 | if ((json === undefined) || (json === null)) { 46 | return json; 47 | } 48 | return { 49 | 50 | 'address': json['address'], 51 | }; 52 | } 53 | 54 | export function ModelServerToJSON(value?: ModelServer | null): any { 55 | if (value === undefined) { 56 | return undefined; 57 | } 58 | if (value === null) { 59 | return null; 60 | } 61 | return { 62 | 63 | 'address': value.address, 64 | }; 65 | } 66 | 67 | -------------------------------------------------------------------------------- /devops-frontend/src/generated-sources/openapi/models/ProtoAction.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * NEW Devops API 5 | * Devops API Sec 6 | * 7 | * The version of the OpenAPI document: v0.1.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import { exists, mapValues } from '../runtime'; 16 | import type { ProtoExecution } from './ProtoExecution'; 17 | import { 18 | ProtoExecutionFromJSON, 19 | ProtoExecutionFromJSONTyped, 20 | ProtoExecutionToJSON, 21 | } from './ProtoExecution'; 22 | import type { StructpbValue } from './StructpbValue'; 23 | import { 24 | StructpbValueFromJSON, 25 | StructpbValueFromJSONTyped, 26 | StructpbValueToJSON, 27 | } from './StructpbValue'; 28 | 29 | /** 30 | * 31 | * @export 32 | * @interface ProtoAction 33 | */ 34 | export interface ProtoAction { 35 | /** 36 | * 37 | * @type {{ [key: string]: StructpbValue; }} 38 | * @memberof ProtoAction 39 | */ 40 | args?: { [key: string]: StructpbValue; }; 41 | /** 42 | * 43 | * @type {ProtoExecution} 44 | * @memberof ProtoAction 45 | */ 46 | execution?: ProtoExecution; 47 | /** 48 | * 49 | * @type {string} 50 | * @memberof ProtoAction 51 | */ 52 | keyBinding?: string; 53 | /** 54 | * 55 | * @type {string} 56 | * @memberof ProtoAction 57 | */ 58 | name?: string; 59 | /** 60 | * 61 | * @type {string} 62 | * @memberof ProtoAction 63 | */ 64 | outputType?: string; 65 | /** 66 | * 67 | * @type {{ [key: string]: StructpbValue; }} 68 | * @memberof ProtoAction 69 | */ 70 | schema?: { [key: string]: StructpbValue; }; 71 | } 72 | 73 | /** 74 | * Check if a given object implements the ProtoAction interface. 75 | */ 76 | export function instanceOfProtoAction(value: object): boolean { 77 | let isInstance = true; 78 | 79 | return isInstance; 80 | } 81 | 82 | export function ProtoActionFromJSON(json: any): ProtoAction { 83 | return ProtoActionFromJSONTyped(json, false); 84 | } 85 | 86 | export function ProtoActionFromJSONTyped(json: any, ignoreDiscriminator: boolean): ProtoAction { 87 | if ((json === undefined) || (json === null)) { 88 | return json; 89 | } 90 | return { 91 | 92 | 'args': !exists(json, 'args') ? undefined : (mapValues(json['args'], StructpbValueFromJSON)), 93 | 'execution': !exists(json, 'execution') ? undefined : ProtoExecutionFromJSON(json['execution']), 94 | 'keyBinding': !exists(json, 'key_binding') ? undefined : json['key_binding'], 95 | 'name': !exists(json, 'name') ? undefined : json['name'], 96 | 'outputType': !exists(json, 'output_type') ? undefined : json['output_type'], 97 | 'schema': !exists(json, 'schema') ? undefined : (mapValues(json['schema'], StructpbValueFromJSON)), 98 | }; 99 | } 100 | 101 | export function ProtoActionToJSON(value?: ProtoAction | null): any { 102 | if (value === undefined) { 103 | return undefined; 104 | } 105 | if (value === null) { 106 | return null; 107 | } 108 | return { 109 | 110 | 'args': value.args === undefined ? undefined : (mapValues(value.args, StructpbValueToJSON)), 111 | 'execution': ProtoExecutionToJSON(value.execution), 112 | 'key_binding': value.keyBinding, 113 | 'name': value.name, 114 | 'output_type': value.outputType, 115 | 'schema': value.schema === undefined ? undefined : (mapValues(value.schema, StructpbValueToJSON)), 116 | }; 117 | } 118 | 119 | -------------------------------------------------------------------------------- /devops-frontend/src/generated-sources/openapi/models/ProtoAuthInfo.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * NEW Devops API 5 | * Devops API Sec 6 | * 7 | * The version of the OpenAPI document: v0.1.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import { exists, mapValues } from '../runtime'; 16 | /** 17 | * 18 | * @export 19 | * @interface ProtoAuthInfo 20 | */ 21 | export interface ProtoAuthInfo { 22 | /** 23 | * 24 | * @type {Array} 25 | * @memberof ProtoAuthInfo 26 | */ 27 | defaultIsolators?: Array; 28 | /** 29 | * 30 | * @type {string} 31 | * @memberof ProtoAuthInfo 32 | */ 33 | identifyingName?: string; 34 | /** 35 | * 36 | * @type {{ [key: string]: string; }} 37 | * @memberof ProtoAuthInfo 38 | */ 39 | info?: { [key: string]: string; }; 40 | /** 41 | * 42 | * @type {boolean} 43 | * @memberof ProtoAuthInfo 44 | */ 45 | isDefault?: boolean; 46 | /** 47 | * 48 | * @type {string} 49 | * @memberof ProtoAuthInfo 50 | */ 51 | name?: string; 52 | /** 53 | * 54 | * @type {string} 55 | * @memberof ProtoAuthInfo 56 | */ 57 | path?: string; 58 | } 59 | 60 | /** 61 | * Check if a given object implements the ProtoAuthInfo interface. 62 | */ 63 | export function instanceOfProtoAuthInfo(value: object): boolean { 64 | let isInstance = true; 65 | 66 | return isInstance; 67 | } 68 | 69 | export function ProtoAuthInfoFromJSON(json: any): ProtoAuthInfo { 70 | return ProtoAuthInfoFromJSONTyped(json, false); 71 | } 72 | 73 | export function ProtoAuthInfoFromJSONTyped(json: any, ignoreDiscriminator: boolean): ProtoAuthInfo { 74 | if ((json === undefined) || (json === null)) { 75 | return json; 76 | } 77 | return { 78 | 79 | 'defaultIsolators': !exists(json, 'default_isolators') ? undefined : json['default_isolators'], 80 | 'identifyingName': !exists(json, 'identifying_name') ? undefined : json['identifying_name'], 81 | 'info': !exists(json, 'info') ? undefined : json['info'], 82 | 'isDefault': !exists(json, 'is_default') ? undefined : json['is_default'], 83 | 'name': !exists(json, 'name') ? undefined : json['name'], 84 | 'path': !exists(json, 'path') ? undefined : json['path'], 85 | }; 86 | } 87 | 88 | export function ProtoAuthInfoToJSON(value?: ProtoAuthInfo | null): any { 89 | if (value === undefined) { 90 | return undefined; 91 | } 92 | if (value === null) { 93 | return null; 94 | } 95 | return { 96 | 97 | 'default_isolators': value.defaultIsolators, 98 | 'identifying_name': value.identifyingName, 99 | 'info': value.info, 100 | 'is_default': value.isDefault, 101 | 'name': value.name, 102 | 'path': value.path, 103 | }; 104 | } 105 | 106 | -------------------------------------------------------------------------------- /devops-frontend/src/generated-sources/openapi/models/ProtoExecution.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * NEW Devops API 5 | * Devops API Sec 6 | * 7 | * The version of the OpenAPI document: v0.1.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import { exists, mapValues } from '../runtime'; 16 | import type { ProtoServerInput } from './ProtoServerInput'; 17 | import { 18 | ProtoServerInputFromJSON, 19 | ProtoServerInputFromJSONTyped, 20 | ProtoServerInputToJSON, 21 | } from './ProtoServerInput'; 22 | import type { ProtoUserInput } from './ProtoUserInput'; 23 | import { 24 | ProtoUserInputFromJSON, 25 | ProtoUserInputFromJSONTyped, 26 | ProtoUserInputToJSON, 27 | } from './ProtoUserInput'; 28 | 29 | /** 30 | * 31 | * @export 32 | * @interface ProtoExecution 33 | */ 34 | export interface ProtoExecution { 35 | /** 36 | * 37 | * @type {string} 38 | * @memberof ProtoExecution 39 | */ 40 | cmd?: string; 41 | /** 42 | * 43 | * @type {boolean} 44 | * @memberof ProtoExecution 45 | */ 46 | isLongRunning?: boolean; 47 | /** 48 | * 49 | * @type {ProtoServerInput} 50 | * @memberof ProtoExecution 51 | */ 52 | serverInput?: ProtoServerInput; 53 | /** 54 | * 55 | * @type {ProtoUserInput} 56 | * @memberof ProtoExecution 57 | */ 58 | userInput?: ProtoUserInput; 59 | } 60 | 61 | /** 62 | * Check if a given object implements the ProtoExecution interface. 63 | */ 64 | export function instanceOfProtoExecution(value: object): boolean { 65 | let isInstance = true; 66 | 67 | return isInstance; 68 | } 69 | 70 | export function ProtoExecutionFromJSON(json: any): ProtoExecution { 71 | return ProtoExecutionFromJSONTyped(json, false); 72 | } 73 | 74 | export function ProtoExecutionFromJSONTyped(json: any, ignoreDiscriminator: boolean): ProtoExecution { 75 | if ((json === undefined) || (json === null)) { 76 | return json; 77 | } 78 | return { 79 | 80 | 'cmd': !exists(json, 'cmd') ? undefined : json['cmd'], 81 | 'isLongRunning': !exists(json, 'is_long_running') ? undefined : json['is_long_running'], 82 | 'serverInput': !exists(json, 'server_input') ? undefined : ProtoServerInputFromJSON(json['server_input']), 83 | 'userInput': !exists(json, 'user_input') ? undefined : ProtoUserInputFromJSON(json['user_input']), 84 | }; 85 | } 86 | 87 | export function ProtoExecutionToJSON(value?: ProtoExecution | null): any { 88 | if (value === undefined) { 89 | return undefined; 90 | } 91 | if (value === null) { 92 | return null; 93 | } 94 | return { 95 | 96 | 'cmd': value.cmd, 97 | 'is_long_running': value.isLongRunning, 98 | 'server_input': ProtoServerInputToJSON(value.serverInput), 99 | 'user_input': ProtoUserInputToJSON(value.userInput), 100 | }; 101 | } 102 | 103 | -------------------------------------------------------------------------------- /devops-frontend/src/generated-sources/openapi/models/ProtoServerInput.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * NEW Devops API 5 | * Devops API Sec 6 | * 7 | * The version of the OpenAPI document: v0.1.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import { exists, mapValues } from '../runtime'; 16 | /** 17 | * 18 | * @export 19 | * @interface ProtoServerInput 20 | */ 21 | export interface ProtoServerInput { 22 | /** 23 | * 24 | * @type {boolean} 25 | * @memberof ProtoServerInput 26 | */ 27 | required?: boolean; 28 | } 29 | 30 | /** 31 | * Check if a given object implements the ProtoServerInput interface. 32 | */ 33 | export function instanceOfProtoServerInput(value: object): boolean { 34 | let isInstance = true; 35 | 36 | return isInstance; 37 | } 38 | 39 | export function ProtoServerInputFromJSON(json: any): ProtoServerInput { 40 | return ProtoServerInputFromJSONTyped(json, false); 41 | } 42 | 43 | export function ProtoServerInputFromJSONTyped(json: any, ignoreDiscriminator: boolean): ProtoServerInput { 44 | if ((json === undefined) || (json === null)) { 45 | return json; 46 | } 47 | return { 48 | 49 | 'required': !exists(json, 'required') ? undefined : json['required'], 50 | }; 51 | } 52 | 53 | export function ProtoServerInputToJSON(value?: ProtoServerInput | null): any { 54 | if (value === undefined) { 55 | return undefined; 56 | } 57 | if (value === null) { 58 | return null; 59 | } 60 | return { 61 | 62 | 'required': value.required, 63 | }; 64 | } 65 | 66 | -------------------------------------------------------------------------------- /devops-frontend/src/generated-sources/openapi/models/ProtoUserInput.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * NEW Devops API 5 | * Devops API Sec 6 | * 7 | * The version of the OpenAPI document: v0.1.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import { exists, mapValues } from '../runtime'; 16 | import type { StructpbValue } from './StructpbValue'; 17 | import { 18 | StructpbValueFromJSON, 19 | StructpbValueFromJSONTyped, 20 | StructpbValueToJSON, 21 | } from './StructpbValue'; 22 | 23 | /** 24 | * 25 | * @export 26 | * @interface ProtoUserInput 27 | */ 28 | export interface ProtoUserInput { 29 | /** 30 | * 31 | * @type {{ [key: string]: StructpbValue; }} 32 | * @memberof ProtoUserInput 33 | */ 34 | args?: { [key: string]: StructpbValue; }; 35 | /** 36 | * 37 | * @type {boolean} 38 | * @memberof ProtoUserInput 39 | */ 40 | required?: boolean; 41 | } 42 | 43 | /** 44 | * Check if a given object implements the ProtoUserInput interface. 45 | */ 46 | export function instanceOfProtoUserInput(value: object): boolean { 47 | let isInstance = true; 48 | 49 | return isInstance; 50 | } 51 | 52 | export function ProtoUserInputFromJSON(json: any): ProtoUserInput { 53 | return ProtoUserInputFromJSONTyped(json, false); 54 | } 55 | 56 | export function ProtoUserInputFromJSONTyped(json: any, ignoreDiscriminator: boolean): ProtoUserInput { 57 | if ((json === undefined) || (json === null)) { 58 | return json; 59 | } 60 | return { 61 | 62 | 'args': !exists(json, 'args') ? undefined : (mapValues(json['args'], StructpbValueFromJSON)), 63 | 'required': !exists(json, 'required') ? undefined : json['required'], 64 | }; 65 | } 66 | 67 | export function ProtoUserInputToJSON(value?: ProtoUserInput | null): any { 68 | if (value === undefined) { 69 | return undefined; 70 | } 71 | if (value === null) { 72 | return null; 73 | } 74 | return { 75 | 76 | 'args': value.args === undefined ? undefined : (mapValues(value.args, StructpbValueToJSON)), 77 | 'required': value.required, 78 | }; 79 | } 80 | 81 | -------------------------------------------------------------------------------- /devops-frontend/src/generated-sources/openapi/models/StructpbValue.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * NEW Devops API 5 | * Devops API Sec 6 | * 7 | * The version of the OpenAPI document: v0.1.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import { exists, mapValues } from '../runtime'; 16 | /** 17 | * 18 | * @export 19 | * @interface StructpbValue 20 | */ 21 | export interface StructpbValue { 22 | /** 23 | * The kind of value. 24 | * 25 | * Types that are assignable to Kind: 26 | * *Value_NullValue 27 | * *Value_NumberValue 28 | * *Value_StringValue 29 | * *Value_BoolValue 30 | * *Value_StructValue 31 | * *Value_ListValue 32 | * @type {object} 33 | * @memberof StructpbValue 34 | */ 35 | kind?: object; 36 | } 37 | 38 | /** 39 | * Check if a given object implements the StructpbValue interface. 40 | */ 41 | export function instanceOfStructpbValue(value: object): boolean { 42 | let isInstance = true; 43 | 44 | return isInstance; 45 | } 46 | 47 | export function StructpbValueFromJSON(json: any): StructpbValue { 48 | return StructpbValueFromJSONTyped(json, false); 49 | } 50 | 51 | export function StructpbValueFromJSONTyped(json: any, ignoreDiscriminator: boolean): StructpbValue { 52 | if ((json === undefined) || (json === null)) { 53 | return json; 54 | } 55 | return { 56 | 57 | 'kind': !exists(json, 'kind') ? undefined : json['kind'], 58 | }; 59 | } 60 | 61 | export function StructpbValueToJSON(value?: StructpbValue | null): any { 62 | if (value === undefined) { 63 | return undefined; 64 | } 65 | if (value === null) { 66 | return null; 67 | } 68 | return { 69 | 70 | 'kind': value.kind, 71 | }; 72 | } 73 | 74 | -------------------------------------------------------------------------------- /devops-frontend/src/generated-sources/openapi/models/index.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export * from './ModelAuthResponse'; 4 | export * from './ModelConfig'; 5 | export * from './ModelErrorResponse'; 6 | export * from './ModelEventResponse'; 7 | export * from './ModelFrontendEvent'; 8 | export * from './ModelInfoResponse'; 9 | export * from './ModelPlugin'; 10 | export * from './ModelServer'; 11 | export * from './ProtoAction'; 12 | export * from './ProtoAuthInfo'; 13 | export * from './ProtoExecution'; 14 | export * from './ProtoServerInput'; 15 | export * from './ProtoUserInput'; 16 | export * from './StructpbValue'; 17 | -------------------------------------------------------------------------------- /devops-frontend/src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | margin: 0; 5 | overflow: hidden; 6 | } 7 | 8 | #root { 9 | height: 100%; 10 | } 11 | 12 | @media (prefers-color-scheme: light) { 13 | :root { 14 | color: #213547; 15 | background-color: #ffffff; 16 | } 17 | } -------------------------------------------------------------------------------- /devops-frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import { Provider } from 'react-redux' 4 | import store from './redux/store' 5 | import App from './App' 6 | import './index.css' 7 | 8 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 9 | // 10 | 11 | 12 | 13 | // , 14 | ) 15 | -------------------------------------------------------------------------------- /devops-frontend/src/pages/Home.css: -------------------------------------------------------------------------------- 1 | .resource-table-container { 2 | height: calc(100vh - 230px); 3 | /* Adjust the value 200px to the actual height of your header, footer, and other components */ 4 | overflow-y: auto; 5 | } -------------------------------------------------------------------------------- /devops-frontend/src/pages/PluginSelector.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharadregoti/devops-cli/81b5f40238d73abc985cbbe82e80b10419bf4b2b/devops-frontend/src/pages/PluginSelector.css -------------------------------------------------------------------------------- /devops-frontend/src/redux/actions/infoActions.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | import { getGeneralInfoService, getAppConfigService, getPluginAuthService } from '../../services/generalInfo'; 3 | import { notify } from '../../utils/utils'; 4 | 5 | export const getGeneralInfoAction = createAsyncThunk( 6 | 'generalInfo/getGeneralInfoAction', 7 | async () => { 8 | return getGeneralInfoService().then(res => { 9 | return res; 10 | }).catch((ex: string) => notify('error', 'Error in getting general info', ex)); 11 | } 12 | ); 13 | 14 | export const getAppConfig = createAsyncThunk( 15 | 'generalInfo/getAppConfig', 16 | async () => { 17 | return getAppConfigService().then(res => { 18 | return res; 19 | }).catch((ex: string) => notify('error', 'Error in getting app config', ex)); 20 | } 21 | ); 22 | 23 | export const getPluginAuth = createAsyncThunk( 24 | 'generalInfo/getPluginAuth', 25 | async (pluginName: string) => { 26 | return getPluginAuthService(pluginName).then(res => { 27 | return res; 28 | }).catch((ex: string) => notify('error', 'Error in getting plugin auth', ex)); 29 | } 30 | ); -------------------------------------------------------------------------------- /devops-frontend/src/redux/reducers/Home.tsx: -------------------------------------------------------------------------------- 1 | import { DrawerPropsTypes } from "../../components/drawer/Drawer"; 2 | import { InfoCardPropsTypes } from "../../components/isolator/Isolator"; 3 | import { SpecificActionFormProps } from "../../components/specificActionForm/SpecificActionForm"; 4 | import { ModelConfig } from "../../generated-sources/openapi"; 5 | import { AppState } from "../../types/Event"; 6 | 7 | 8 | export type HomeState = { 9 | home: Record; 17 | }; 18 | 19 | export const homeReducer = ( 20 | state: HomeState['home'] = {}, 21 | action: { type: string; payload: any; key: string } 22 | ) => { 23 | switch (action.type) { 24 | case 'ADD_ISOLATOR': { 25 | const { key, isolatorName } = action; 26 | const currentIsolatorsList = state[key]?.isolatorsList || []; 27 | 28 | // Check if isolatorName already exists in the currentIsolatorsList 29 | if (currentIsolatorsList.includes(isolatorName)) { 30 | return state; // Return the unchanged state if the isolator already exists 31 | } 32 | 33 | return { 34 | ...state, 35 | [key]: { 36 | ...state[key], 37 | isolatorsList: [...currentIsolatorsList, isolatorName], 38 | }, 39 | }; 40 | } 41 | case 'SET_HOME_STATE': 42 | return { 43 | ...state, 44 | [action.key]: { 45 | ...(state[action.key] || { 46 | recentlyUsedItems: [], 47 | isolatorsList: [], 48 | drawerState: {}, 49 | specificActionFormState: { 50 | formItems: {}, 51 | } as SpecificActionFormProps, 52 | isolatorCardState: {} as InfoCardPropsTypes, 53 | }), 54 | ...action.payload, 55 | }, 56 | }; 57 | default: 58 | return state; 59 | } 60 | }; 61 | 62 | 63 | // export type NavBarState = { 64 | // navBar: { 65 | // items: NavBarItem[]; 66 | // }; 67 | // }; 68 | 69 | // export type NavBarItem = { 70 | // pluginName: string; 71 | // authId: string; 72 | // contextId: string; 73 | // sessionId: string; 74 | // } 75 | 76 | // export const navBarReducer = (state = { items: [{ pluginName: "", sessionId: "0", authId: "", contextId: "Plugins" } as NavBarItem] }, action) => { 77 | // switch (action.type) { 78 | // case 'SET_NAV_BAR_STATE': 79 | // return { ...state, ...action.payload }; 80 | // default: 81 | // return state; 82 | // } 83 | // }; 84 | 85 | -------------------------------------------------------------------------------- /devops-frontend/src/redux/reducers/PluginSelectorReducer.tsx: -------------------------------------------------------------------------------- 1 | import { ModelConfig, ModelInfoResponse } from "../../generated-sources/openapi"; 2 | 3 | 4 | export type PluginSelectorState = { 5 | pluginSelector: { 6 | pluginName: string; 7 | serverConfig: ModelConfig, 8 | pluginAuthData: TableDataType[]; 9 | }; 10 | }; 11 | 12 | export interface TableDataType { 13 | key: string; 14 | name: string; 15 | context: string; 16 | } 17 | 18 | export const pluginSelectorReducer = (state = { pluginName: "", serverConfig: {}, pluginAuthData: [] }, action) => { 19 | switch (action.type) { 20 | case 'SET_PLUGIN_SELECTOR_STATE': 21 | return { ...state, ...action.payload }; 22 | default: 23 | return state; 24 | } 25 | }; 26 | 27 | export type NavBarState = { 28 | navBar: { 29 | items: NavBarItem[]; 30 | }; 31 | }; 32 | 33 | export type NavBarItem = { 34 | pluginName: string; 35 | authId: string; 36 | contextId: string; 37 | sessionId: string; 38 | generalInfo: ModelInfoResponse 39 | } 40 | 41 | export const navBarReducer = (state = { items: [{ pluginName: "", sessionId: "0", authId: "", contextId: "Plugins", generalInfo: {} } as NavBarItem] }, action) => { 42 | switch (action.type) { 43 | case 'SET_NAV_BAR_STATE': 44 | return { ...state, ...action.payload }; 45 | default: 46 | return state; 47 | } 48 | }; 49 | 50 | -------------------------------------------------------------------------------- /devops-frontend/src/redux/reducers/infoReducer.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { InfoCardTypes } from '../../types/InfoCardTypes'; 3 | import { failed, loading, success } from '../../utils/constants'; 4 | import { getGeneralInfoAction } from '../actions/infoActions'; 5 | 6 | interface InitialStateTypes { 7 | info: InfoCardTypes | null, 8 | status: string | null 9 | } 10 | const initialState= { 11 | info: null, 12 | status: null 13 | } as InitialStateTypes 14 | 15 | export const generalInfoSlice = createSlice({ 16 | name: 'generalInfo', 17 | initialState, 18 | reducers: {}, 19 | extraReducers: (builder) => { 20 | builder.addCase(getGeneralInfoAction.pending, (state) => { 21 | state.status = loading; 22 | }) 23 | .addCase(getGeneralInfoAction.fulfilled, (state, action: PayloadAction) => { 24 | state.status = success; 25 | state.info = action.payload; 26 | }) 27 | .addCase(getGeneralInfoAction.rejected, (state) => { 28 | state.status = failed; 29 | }); 30 | } 31 | }); 32 | 33 | export default generalInfoSlice.reducer; 34 | -------------------------------------------------------------------------------- /devops-frontend/src/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import { combineReducers } from 'redux'; 3 | import generalInfoReducer from './reducers/infoReducer'; 4 | import { ModelConfig } from "../generated-sources/openapi"; 5 | import { navBarReducer, pluginSelectorReducer } from "./reducers/PluginSelectorReducer"; 6 | import { homeReducer } from "./reducers/Home"; 7 | 8 | const rootReducer = combineReducers({ 9 | pluginSelector: pluginSelectorReducer, 10 | navBar: navBarReducer, 11 | home: homeReducer, 12 | }); 13 | 14 | const store = configureStore({ 15 | reducer: rootReducer, 16 | devTools: true 17 | }); 18 | 19 | export default store; 20 | 21 | export type StoreType = ReturnType; 22 | 23 | export type DispatchType = typeof store.dispatch; -------------------------------------------------------------------------------- /devops-frontend/src/redux/typedReduxHook.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; 2 | import { DispatchType, StoreType } from "./store"; 3 | 4 | export const useAppDispatch: () => DispatchType = useDispatch; 5 | export const useAppSelector: TypedUseSelectorHook = useSelector; -------------------------------------------------------------------------------- /devops-frontend/src/services/generalInfo.ts: -------------------------------------------------------------------------------- 1 | import api from "./index"; 2 | import { AxiosError, AxiosResponse } from "axios"; 3 | import { InfoCardTypes } from "../types/InfoCardTypes"; 4 | 5 | export const getGeneralInfoService = (pluginName: string, authId: string, contextId: string) => { 6 | return new Promise((resolve, reject) => { 7 | api.get(`/connect/${pluginName}/${authId}/${contextId}`).then((res: AxiosResponse) => { 8 | if(res.statusText === 'OK'){ 9 | resolve(res.data); 10 | } 11 | }).catch((ex: AxiosError) => reject(ex)) 12 | }) 13 | } 14 | 15 | export const getAppConfigService = () => { 16 | return new Promise((resolve, reject) => { 17 | api.get('/config').then((res: AxiosResponse) => { 18 | if(res.statusText === 'OK'){ 19 | resolve(res.data); 20 | } 21 | }).catch((ex: AxiosError) => reject(ex)) 22 | }) 23 | } 24 | 25 | export const getPluginAuthService = (pluginName: string) => { 26 | return new Promise((resolve, reject) => { 27 | api.get(`/auth/${pluginName}`).then((res: AxiosResponse) => { 28 | if(res.statusText === 'OK'){ 29 | resolve(res.data); 30 | } 31 | }).catch((ex: AxiosError) => reject(ex)) 32 | }) 33 | } 34 | 35 | export const sendEvent = (id: string, data: any) => { 36 | return new Promise((resolve, reject) => { 37 | api.post(`/events/${id}`, data).then((res: AxiosResponse) => { 38 | if(res.statusText === 'OK'){ 39 | resolve(res.data); 40 | } 41 | }).catch((ex: AxiosError) => reject(ex)) 42 | }) 43 | } -------------------------------------------------------------------------------- /devops-frontend/src/services/index.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const api = axios.create({ 4 | baseURL: 'http://localhost:9753/v1', 5 | }); 6 | 7 | export default api; -------------------------------------------------------------------------------- /devops-frontend/src/types/Event.ts: -------------------------------------------------------------------------------- 1 | import { ModelAuthResponse, ModelConfig, ModelInfoResponse, ProtoAction } from "../generated-sources/openapi"; 2 | 3 | export interface TableEvent { 4 | eventType: string; 5 | record: any; 6 | } 7 | 8 | export interface AppState { 9 | serverConfig: ModelConfig, 10 | pluginAuth: ModelAuthResponse, 11 | generalInfo: ModelInfoResponse, 12 | currentIsolator: string, 13 | currentResourceType: string, 14 | currentPluginName: string 15 | } 16 | 17 | export interface WebsocketData { 18 | id: string; 19 | name: string; 20 | eventType: string; 21 | data: Datum[]; 22 | specificActions: Array; 23 | } 24 | 25 | export interface Datum { 26 | data: string[]; 27 | color: string; 28 | } 29 | 30 | export interface SpecificAction { 31 | name: string; 32 | key_binding: string; 33 | output_type: string; 34 | execution: Execution 35 | } 36 | 37 | export interface Execution { 38 | cmd: string; 39 | user_input: UserInput; 40 | } 41 | 42 | export interface UserInput { 43 | required: boolean; 44 | args: object; 45 | } -------------------------------------------------------------------------------- /devops-frontend/src/types/InfoCardTypes.ts: -------------------------------------------------------------------------------- 1 | export interface InfoCardTypes { 2 | id: string; 3 | general: General; 4 | plugins: Plugins; 5 | actions: Action[]; 6 | resourceTypes: string[]; 7 | defaultIsolator: string; 8 | isolatorType: string; 9 | } 10 | 11 | export interface Action { 12 | type: string; 13 | name: string; 14 | key_binding: string; 15 | output_type: string; 16 | schema?: any; 17 | } 18 | 19 | export interface Plugins { 20 | 'alt-0': string; 21 | } 22 | 23 | export interface General { 24 | Cluster: string; 25 | Context: string; 26 | 'Server Version': string; 27 | User: string; 28 | } -------------------------------------------------------------------------------- /devops-frontend/src/types/ResourceTypes.ts: -------------------------------------------------------------------------------- 1 | export interface ResourceTypes { 2 | id: string; 3 | name: string; 4 | data: Datum[]; 5 | specificActions: SpecificAction[]; 6 | } 7 | 8 | interface SpecificAction { 9 | name: string; 10 | key_binding: string; 11 | output_type: string; 12 | args?: any; 13 | schema?: any; 14 | execution: Execution; 15 | } 16 | 17 | interface Execution { 18 | cmd: string; 19 | is_long_running: boolean; 20 | user_input: Userinput; 21 | server_input: Serverinput; 22 | } 23 | 24 | interface Serverinput { 25 | required: boolean; 26 | } 27 | 28 | interface Userinput { 29 | required: boolean; 30 | args?: any; 31 | } 32 | 33 | interface Datum { 34 | data: string[]; 35 | color: string; 36 | } -------------------------------------------------------------------------------- /devops-frontend/src/types/utilsTypes.ts: -------------------------------------------------------------------------------- 1 | export type notifyTypes = (type: string, 2 | title: string, 3 | msg: string, 4 | duration?: number) => void; -------------------------------------------------------------------------------- /devops-frontend/src/utils/config.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, DefaultApi, ModelConfig, HandleAuthRequest, HandleInfoRequest, HandleEventRequest, ModelFrontendEvent, ModelFrontendEventNameEnum, ProtoAction } from "../generated-sources/openapi"; 2 | 3 | // http://localhost:9753 4 | // For Prod 5 | export const apiHost = window.location.host; 6 | // export const apiHost = "localhost:9753"; 7 | export const httpAPI = `http://${apiHost}`; 8 | 9 | const configuration = new Configuration({ 10 | basePath: `${httpAPI}`, 11 | }); 12 | 13 | export const api = new DefaultApi(configuration); -------------------------------------------------------------------------------- /devops-frontend/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const loading = 'LOADING'; 2 | 3 | export const success = 'SUCCESS'; 4 | 5 | export const failed = 'FAILED'; -------------------------------------------------------------------------------- /devops-frontend/src/utils/notification.ts: -------------------------------------------------------------------------------- 1 | import { notification } from 'antd'; 2 | 3 | 4 | type NotificationType = 'success' | 'info' | 'warning' | 'error'; 5 | 6 | export const showNotification = (nType: NotificationType, title: string, description: string) => { 7 | 8 | switch (nType) { 9 | case 'success': 10 | notification.success({ 11 | message: title, 12 | description: description, 13 | }); 14 | break; 15 | case 'info': 16 | notification.info({ 17 | message: title, 18 | description: description, 19 | }); 20 | break; 21 | case 'warning': 22 | notification.warning({ 23 | message: title, 24 | description: description, 25 | }); 26 | break; 27 | case 'error': 28 | notification.error({ 29 | message: title, 30 | description: description, 31 | }); 32 | break; 33 | } 34 | }; -------------------------------------------------------------------------------- /devops-frontend/src/utils/settings.ts: -------------------------------------------------------------------------------- 1 | // Constants 2 | export const settingPlugin = "@use.plugin"; 3 | export const settingAuthentication = "@use.auth"; 4 | 5 | // A function which returns a string for use.plugin 6 | export function getPluginSetting(name: string) { 7 | return `${settingPlugin}.${name}`; 8 | } 9 | 10 | // A function to parse the plugin setting and extract the plugin name 11 | export function parsePluginSetting(setting: string) { 12 | return setting.split(".")[2]; 13 | } 14 | 15 | // A function to parse the authentication setting and extract identifyingName and name 16 | export function parseAuthenticationSetting(setting: string) { 17 | const arr = setting.split("."); 18 | return { 19 | "identifyingName": arr[2], 20 | "name": arr[3] 21 | } 22 | } 23 | 24 | // A function which returns a string for authentication setting 25 | export function getAuthenticationSetting(identifyingName: string, name: string) { 26 | return `${settingAuthentication}.${identifyingName}.${name}`; 27 | } 28 | -------------------------------------------------------------------------------- /devops-frontend/src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { notification } from "antd"; 2 | import { notifyTypes } from "../types/utilsTypes"; 3 | 4 | export const notify: notifyTypes = (type, title, msg, duration) => { 5 | // notification.config({ message: title, description: String(msg), duration: duration }); 6 | } -------------------------------------------------------------------------------- /devops-frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /devops-frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /devops-frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /devops-frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /devops-plugin-sdk/devops_plugin.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/sharadregoti/devops-plugin-sdk/proto" 7 | 8 | "github.com/hashicorp/go-hclog" 9 | "github.com/hashicorp/go-plugin" 10 | "google.golang.org/grpc" 11 | ) 12 | 13 | type DevopsPlugin struct { 14 | plugin.NetRPCUnsupportedPlugin 15 | Logger hclog.Logger 16 | Impl Devops 17 | } 18 | 19 | func (p *DevopsPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { 20 | proto.RegisterDevopsServer(s, &GRPCServer{Impl: p.Impl, Logger: p.Logger}) 21 | return nil 22 | } 23 | 24 | func (p *DevopsPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { 25 | var _ Devops = &GRPCClient{} 26 | return &GRPCClient{client: proto.NewDevopsClient(c)}, nil 27 | } 28 | -------------------------------------------------------------------------------- /devops-plugin-sdk/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sharadregoti/devops-plugin-sdk 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/golang/protobuf v1.5.2 7 | github.com/hashicorp/go-hclog v1.4.0 8 | google.golang.org/grpc v1.53.0 9 | ) 10 | 11 | require ( 12 | github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect 13 | github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 // indirect 14 | github.com/oklog/run v1.0.0 // indirect 15 | golang.org/x/net v0.6.0 // indirect 16 | golang.org/x/text v0.7.0 // indirect 17 | google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc // indirect 18 | ) 19 | 20 | require ( 21 | github.com/fatih/color v1.13.0 // indirect 22 | github.com/hashicorp/go-plugin v1.4.8 23 | github.com/mattn/go-colorable v0.1.12 // indirect 24 | github.com/mattn/go-isatty v0.0.14 // indirect 25 | golang.org/x/sys v0.5.0 // indirect 26 | google.golang.org/protobuf v1.28.1 27 | ) 28 | -------------------------------------------------------------------------------- /devops-plugin-sdk/interface.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | import "github.com/sharadregoti/devops-plugin-sdk/proto" 4 | 5 | type Devops interface { 6 | Name() string 7 | MainBox 8 | SearchBox 9 | GeneralInfoBox 10 | ResourceIsolatorBox 11 | GenericResourceActions 12 | ResourceSpecificActions 13 | } 14 | 15 | // ############################# MainBox ############################# 16 | 17 | type GetResourcesArgs struct { 18 | ResourceName, ResourceType, IsolatorID string 19 | Args map[string]interface{} 20 | } 21 | 22 | type MainBox interface { 23 | GetResources(args *proto.GetResourcesArgs) ([]interface{}, error) 24 | WatchResources(args *proto.GetResourcesArgs) (chan WatchResourceResult, chan struct{}, error) 25 | CloseResourceWatcher(resourceType string) error 26 | GetResourceTypeSchema(resourceType string) (*proto.ResourceTransformer, error) 27 | } 28 | 29 | // ############################# SearchBox ############################# 30 | 31 | type SearchBox interface { 32 | GetResourceTypeList() ([]string, error) 33 | } 34 | 35 | // type AuthInfo struct { 36 | // IdentifyingName string 37 | // Name string 38 | // IsDefault bool 39 | // DefaultIsolators []string 40 | // Info map[string]string 41 | // } 42 | 43 | type GeneralInfoBox interface { 44 | GetAuthInfo() (*proto.AuthInfoResponse, error) 45 | Connect(authInfo *proto.AuthInfo) error 46 | } 47 | 48 | type ResourceIsolatorBox interface { 49 | GetResourceIsolatorType() (string, error) 50 | GetDefaultResourceIsolator() (string, error) 51 | } 52 | 53 | // type DebuggingBox interface { 54 | // GetResourceTypeConditions() error 55 | // } 56 | 57 | // type ChatGPTBox interface { 58 | // GetResourceTypeConditions() error 59 | // } 60 | 61 | // type ActionDeleteResourceArgs struct { 62 | // ResourceName, ResourceType, IsolatorName string 63 | // } 64 | 65 | // type ActionCreateResourceArgs struct { 66 | // ResourceName, ResourceType, IsolatorName string 67 | // Data interface{} 68 | // } 69 | 70 | // type ActionUpdateResourceArgs struct { 71 | // ResourceName, ResourceType, IsolatorName string 72 | // Data interface{} 73 | // } 74 | 75 | type GenericResourceActions interface { 76 | GetSupportedActions() (*proto.GetActionListResponse, error) 77 | ActionDeleteResource(*proto.ActionDeleteResourceArgs) error 78 | ActionCreateResource(*proto.ActionCreateResourceArgs) error 79 | ActionUpdateResource(*proto.ActionUpdateResourceArgs) error 80 | } 81 | 82 | // type SpecificActionArgs struct { 83 | // ActionName string 84 | 85 | // ResourceName string 86 | // ResourceType string 87 | 88 | // IsolatorName string 89 | 90 | // Args map[string]interface{} 91 | // } 92 | 93 | // type SpecificActionResult struct { 94 | // // Temp string 95 | // Result interface{} 96 | // OutputType string 97 | // } 98 | 99 | type ResourceSpecificActions interface { 100 | GetSpecficActionList(resourceType string) (*proto.GetActionListResponse, error) 101 | PerformSpecificAction(args *proto.SpecificActionArgs) (*proto.SpecificActionResult, error) 102 | } 103 | -------------------------------------------------------------------------------- /devops-plugin-sdk/logger.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/hashicorp/go-hclog" 8 | ) 9 | 10 | var Logger hclog.Logger 11 | 12 | func LogInfo(msg string, args ...interface{}) { 13 | if len(args) > 0 { 14 | Logger.Info(fmt.Sprintf(msg, args...)) 15 | } else { 16 | Logger.Info(msg) 17 | } 18 | } 19 | 20 | func LogDebug(msg string, args ...interface{}) { 21 | if len(args) > 0 { 22 | Logger.Debug(fmt.Sprintf(msg, args...)) 23 | } else { 24 | Logger.Debug(msg) 25 | } 26 | } 27 | 28 | func LogTrace(msg string, args ...interface{}) { 29 | if len(args) > 0 { 30 | Logger.Trace(fmt.Sprintf(msg, args...)) 31 | } else { 32 | Logger.Trace(msg) 33 | } 34 | } 35 | 36 | func LogError(msg string, args ...interface{}) error { 37 | if len(args) > 0 { 38 | Logger.Error(fmt.Sprintf(msg, args...)) 39 | return fmt.Errorf(fmt.Sprintf(msg, args...)) 40 | } else { 41 | Logger.Error(msg) 42 | return fmt.Errorf(msg) 43 | } 44 | } 45 | 46 | func GetHCLLogLevel() hclog.Level { 47 | switch os.Getenv("LOG_LEVEL") { 48 | case "trace": 49 | return hclog.Trace 50 | case "debug": 51 | return hclog.Debug 52 | case "info": 53 | return hclog.Info 54 | case "warn": 55 | return hclog.Warn 56 | case "error": 57 | return hclog.Error 58 | } 59 | 60 | return hclog.Debug 61 | } 62 | -------------------------------------------------------------------------------- /devops-plugin-sdk/types.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | type WatchResourceResult struct { 4 | Type string 5 | Result interface{} 6 | } 7 | 8 | type ResourceTransformer struct { 9 | Operations []Operations `json:"operations" yaml:"operations"` 10 | SpecificActions []Action `json:"specific_actions" yaml:"specific_actions"` 11 | Styles []Styles `json:"styles" yaml:"styles"` 12 | Nesting Nesting `json:"nesting" yaml:"nesting"` 13 | } 14 | 15 | type Operations struct { 16 | Name string `json:"name" yaml:"name"` 17 | JSONPaths []JSONPaths `json:"json_paths" yaml:"json_paths"` 18 | OutputFormat string `json:"output_format,omitempty" yaml:"output_format,omitempty"` 19 | } 20 | 21 | type JSONPaths struct { 22 | Path string `json:"path" yaml:"path"` 23 | } 24 | 25 | type Styles struct { 26 | RowBackgroundColor string `json:"row_background_color" yaml:"row_background_color"` 27 | Conditions []string `json:"conditions" yaml:"conditions"` 28 | } 29 | 30 | type Nesting struct { 31 | IsNested bool `json:"is_nested" yaml:"is_nested"` 32 | ResourceType string `json:"resource_type" yaml:"resource_type"` 33 | Args map[string]interface{} `json:"args" yaml:"args"` 34 | IsSelfContainedInParent bool `json:"is_self_contained_in_parent" yaml:"is_self_contained_in_parent"` 35 | ParentDataPaths []string `json:"parent_data_paths" yaml:"parent_data_paths"` 36 | } 37 | 38 | type Action struct { 39 | Name string `json:"name" yaml:"name"` 40 | KeyBinding string `json:"key_binding" yaml:"key_binding"` 41 | // ScrrenAction string `json:"scrren_action" yaml:"scrren_action"` 42 | OutputType string `json:"output_type" yaml:"output_type"` 43 | Args map[string]interface{} `json:"args" yaml:"args"` 44 | Schema map[string]interface{} `json:"schema" yaml:"schema"` 45 | Execution Execution `json:"execution" yaml:"execution"` 46 | } 47 | 48 | type Execution struct { 49 | Cmd string `json:"cmd" yaml:"cmd"` 50 | IsLongRunning bool `json:"is_long_running" yaml:"is_long_running"` 51 | UserInput UserInput `json:"user_input" yaml:"user_input"` 52 | ServerInput ServerInput `json:"server_input" yaml:"server_input"` 53 | } 54 | 55 | type UserInput struct { 56 | Required bool `json:"required" yaml:"required"` 57 | Args map[string]interface{} `json:"args" yaml:"args"` 58 | } 59 | 60 | type ServerInput struct { 61 | Required bool `json:"required" yaml:"required"` 62 | } 63 | -------------------------------------------------------------------------------- /internal/pluginmanager/helpers.go: -------------------------------------------------------------------------------- 1 | package pluginmanager 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/sharadregoti/devops/common" 8 | "github.com/sharadregoti/devops/model" 9 | ) 10 | 11 | func getPluginPath(name, devopsDir string) string { 12 | if common.Release { 13 | return fmt.Sprintf("%s/plugins/%s/%s", devopsDir, name, name) 14 | } 15 | return fmt.Sprintf("../../plugins/%s/%s", name, name) 16 | } 17 | 18 | func ListPlugins() ([]*model.Plugin, error) { 19 | devopsDir := model.InitCoreDirectory() 20 | 21 | var path string = "../../plugins" 22 | if common.Release { 23 | path = fmt.Sprintf("%s/plugins", devopsDir) 24 | } 25 | 26 | entries, err := os.ReadDir(path) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | var plugins []*model.Plugin 32 | for _, entry := range entries { 33 | if entry.IsDir() { 34 | plugins = append(plugins, &model.Plugin{Name: entry.Name(), IsDefault: entry.Name() == "kubernetes"}) 35 | } 36 | } 37 | return plugins, nil 38 | } 39 | -------------------------------------------------------------------------------- /internal/pluginmanager/plugin_client.go: -------------------------------------------------------------------------------- 1 | package pluginmanager 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "os/exec" 8 | 9 | hclog "github.com/hashicorp/go-hclog" 10 | "github.com/hashicorp/go-plugin" 11 | shared "github.com/sharadregoti/devops-plugin-sdk" 12 | "github.com/sharadregoti/devops/model" 13 | ) 14 | 15 | type PluginClient struct { 16 | client plugin.ClientProtocol 17 | logger hclog.Logger 18 | 19 | name string 20 | 21 | stdErrReader *io.PipeReader 22 | stdErrWriter *io.PipeWriter 23 | 24 | stdOutReader *io.PipeReader 25 | stdOutWriter *io.PipeWriter 26 | } 27 | 28 | // handshakeConfigs are used to just do a basic handshake between 29 | // a plugin and host. If the handshake fails, a user friendly error is shown. 30 | // This prevents users from executing bad plugins or executing a plugin 31 | // directory. It is a UX feature, not a security feature. 32 | var handshakeConfig = plugin.HandshakeConfig{ 33 | ProtocolVersion: 1, 34 | MagicCookieKey: "BASIC_PLUGIN", 35 | MagicCookieValue: "hello", 36 | } 37 | 38 | // pluginMap is the map of plugins we can dispense. 39 | var pluginMap = map[string]plugin.Plugin{ 40 | "helm": &shared.DevopsPlugin{}, 41 | "kubernetes": &shared.DevopsPlugin{}, 42 | } 43 | 44 | func ValidatePlugins(c *model.Config) error { 45 | devopsDir := model.InitCoreDirectory() 46 | 47 | for _, p := range c.Plugins { 48 | fmt.Printf("Checking plugin %s\n", p.Name) 49 | _, err := os.Stat(getPluginPath(p.Name, devopsDir)) 50 | if os.IsNotExist(err) { 51 | return fmt.Errorf("Plugin %s does not exists, use devops init command to install the plugin", p.Name) 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | 58 | func startPlugin(logger hclog.Logger, pluginName, rootDir string) (*PluginClient, error) { 59 | path := getPluginPath(pluginName, rootDir) 60 | logger.Debug("Pluging path", path) 61 | 62 | var reader, writer = io.Pipe() 63 | 64 | // We're a host! Start by launching the plugin process. 65 | client := plugin.NewClient(&plugin.ClientConfig{ 66 | HandshakeConfig: handshakeConfig, 67 | Plugins: pluginMap, 68 | Cmd: exec.Command(path), 69 | Logger: logger, 70 | SyncStdout: writer, 71 | SyncStderr: writer, 72 | AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, 73 | }) 74 | // TODO: Closer this as well in the Close function 75 | // defer client.Kill() 76 | 77 | if client.Exited() { 78 | str := fmt.Sprintf("%s plugin exited", pluginName) 79 | return nil, fmt.Errorf(str) 80 | } 81 | 82 | // Connect via GRPC 83 | rpcClient, err := client.Client() 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | return &PluginClient{ 89 | name: pluginName, 90 | // TODO: Set error writer as well 91 | stdErrReader: nil, 92 | stdErrWriter: nil, 93 | stdOutReader: reader, 94 | stdOutWriter: writer, 95 | client: rpcClient, 96 | logger: logger, 97 | }, nil 98 | } 99 | 100 | func (p *PluginClient) GetStdoutReader() *io.PipeReader { 101 | return p.stdOutReader 102 | } 103 | 104 | func (p *PluginClient) GetStdoutWriter() *io.PipeWriter { 105 | return p.stdOutWriter 106 | } 107 | 108 | func (p *PluginClient) Close() { 109 | // TODO: close error writer as well 110 | p.stdOutReader.Close() 111 | p.stdOutWriter.Close() 112 | p.client.Close() 113 | } 114 | 115 | func (p *PluginClient) GetPlugin(name string) (shared.Devops, error) { 116 | // Request the plugin 117 | raw, err := p.client.Dispense(name) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | return raw.(shared.Devops), nil 123 | } 124 | -------------------------------------------------------------------------------- /internal/pluginmanager/session.go: -------------------------------------------------------------------------------- 1 | package pluginmanager 2 | 3 | import ( 4 | "fmt" 5 | "syscall" 6 | 7 | "github.com/gorilla/websocket" 8 | "github.com/sharadregoti/devops-plugin-sdk/proto" 9 | "github.com/sharadregoti/devops/model" 10 | "github.com/sharadregoti/devops/utils/logger" 11 | ) 12 | 13 | type SessionManager struct { 14 | conf *model.Config 15 | m map[string]*sessionInfo 16 | } 17 | 18 | type sessionInfo struct { 19 | c *CurrentPluginContext 20 | } 21 | 22 | func NewSM(conf *model.Config) (*SessionManager, error) { 23 | return &SessionManager{ 24 | conf: conf, 25 | m: make(map[string]*sessionInfo), 26 | }, nil 27 | } 28 | 29 | func (s *SessionManager) SessionCount() int { 30 | return len(s.m) 31 | } 32 | 33 | func (s *SessionManager) DeleteClient(ID string) { 34 | logger.LogInfo("Deleting client with id (%s)", ID) 35 | pCtx, err := s.GetClient(ID) 36 | if err != nil { 37 | return 38 | } 39 | pCtx.Close() 40 | pCtx.wsConn.Close() 41 | delete(s.m, ID) 42 | } 43 | 44 | func (s *SessionManager) SetWSConn(ID string, conn *websocket.Conn) { 45 | info := s.m[ID] 46 | if info.c.wsConn != nil { 47 | logger.LogInfo("Closing old websocket connection") 48 | info.c.wsConn.Close() 49 | } 50 | info.c.wsConn = conn 51 | } 52 | 53 | func (s *SessionManager) KillAllLongSessions() { 54 | fmt.Println("Killing all sessions") 55 | for _, m := range s.m { 56 | for _, lri := range m.c.longRunning { 57 | fmt.Println("Killing process with pid", lri.GetCMD().Process.Pid) 58 | syscall.Kill(-lri.GetCMD().Process.Pid, syscall.SIGTERM) 59 | } 60 | } 61 | } 62 | 63 | func (s *SessionManager) GetClient(ID string) (*CurrentPluginContext, error) { 64 | info, ok := s.m[ID] 65 | if !ok { 66 | return nil, fmt.Errorf("session with this id does not exists") 67 | } 68 | 69 | return info.c, nil 70 | } 71 | 72 | func (s *SessionManager) AddClient(ID, pluginName, authID, contextID string) error { 73 | _, ok := s.m[ID] 74 | if !ok { 75 | pCtx, err := Start(pluginName, s.conf, &proto.AuthInfo{IdentifyingName: authID, Name: contextID}) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | dataPipe := make(chan model.WebsocketResponse, 1) 81 | pCtx.SetDataPipe(dataPipe) 82 | 83 | s.m[ID] = &sessionInfo{ 84 | c: pCtx, 85 | } 86 | logger.LogInfo("New client with ID (%s) is added", ID) 87 | return nil 88 | } 89 | return fmt.Errorf("session with id (%s) already exists", ID) 90 | } 91 | -------------------------------------------------------------------------------- /internal/pluginmanager/table_stack.go: -------------------------------------------------------------------------------- 1 | package pluginmanager 2 | 3 | import ( 4 | shared "github.com/sharadregoti/devops-plugin-sdk" 5 | "github.com/sharadregoti/devops-plugin-sdk/proto" 6 | ) 7 | 8 | type tableStack []*resourceStack 9 | 10 | func (t *tableStack) length() int { 11 | return len(*t) 12 | } 13 | 14 | func (t *tableStack) upsert(index int, r resourceStack) { 15 | for i, _ := range *t { 16 | if i == index { 17 | (*t)[0] = &r 18 | return 19 | } 20 | } 21 | 22 | // Add if does not exists 23 | *t = append(*t, &r) 24 | } 25 | 26 | func (t *tableStack) resetToParentResource() { 27 | // Only get the first element 28 | if len(*t) == 0 { 29 | return 30 | } 31 | *t = (*t)[0:1] 32 | } 33 | 34 | type resourceStack struct { 35 | tableRowNumber int 36 | nextResourceArgs []map[string]interface{} 37 | currentResourceType string 38 | currentResources []interface{} 39 | currentSchema *proto.ResourceTransformer 40 | currentSpecficActionList *proto.GetActionListResponse 41 | } 42 | 43 | type nestedResurce struct { 44 | nextResourceArgs []map[string]interface{} 45 | currentIsolator string 46 | currentResourceType string 47 | currentResources []interface{} 48 | currentSchema shared.ResourceTransformer 49 | currentSpecficActionList []shared.Action 50 | } 51 | -------------------------------------------------------------------------------- /internal/transformer/transformer_test.go: -------------------------------------------------------------------------------- 1 | package transformer 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/go-test/deep" 8 | "github.com/sharadregoti/devops/internal/transformer/testdata" 9 | "github.com/sharadregoti/devops/model" 10 | ) 11 | 12 | func TestGetResourceInTableFormat(t *testing.T) { 13 | 14 | data := map[string]interface{}{} 15 | _ = json.Unmarshal([]byte(testdata.PodJSON), &data) 16 | 17 | type args struct { 18 | t *model.ResourceTransfomer 19 | resources []interface{} 20 | } 21 | tests := []struct { 22 | name string 23 | args args 24 | want [][]string 25 | }{ 26 | { 27 | name: "pass", 28 | args: args{ 29 | t: &model.ResourceTransfomer{ 30 | Operations: []model.Operations{ 31 | { 32 | Name: "namespace", 33 | JSONPaths: []model.JSONPaths{ 34 | { 35 | Path: "metadata.namespace", 36 | }, 37 | }, 38 | OutputFormat: "", 39 | }, 40 | { 41 | Name: "name", 42 | JSONPaths: []model.JSONPaths{ 43 | { 44 | Path: "metadata.name", 45 | }, 46 | }, 47 | OutputFormat: "", 48 | }, 49 | { 50 | Name: "ready", 51 | JSONPaths: []model.JSONPaths{ 52 | { 53 | Path: "status.containerStatuses.#(ready==true)#|#", 54 | }, 55 | { 56 | Path: "status.containerStatuses.#", 57 | }, 58 | }, 59 | OutputFormat: "%v/%v", 60 | }, 61 | { 62 | Name: "restarts", 63 | JSONPaths: []model.JSONPaths{ 64 | { 65 | Path: "status.containerStatuses.0.restartCount", 66 | }, 67 | }, 68 | OutputFormat: "", 69 | }, 70 | { 71 | Name: "status", 72 | JSONPaths: []model.JSONPaths{ 73 | { 74 | Path: "status.phase", 75 | }, 76 | }, 77 | OutputFormat: "", 78 | }, 79 | { 80 | Name: "ip", 81 | JSONPaths: []model.JSONPaths{ 82 | { 83 | Path: "status.podIP", 84 | }, 85 | }, 86 | OutputFormat: "", 87 | }, 88 | { 89 | Name: "node", 90 | JSONPaths: []model.JSONPaths{ 91 | { 92 | Path: "spec.nodeName", 93 | }, 94 | }, 95 | OutputFormat: "", 96 | }, 97 | { 98 | Name: "age", 99 | JSONPaths: []model.JSONPaths{ 100 | { 101 | Path: "status.startTime|@age", 102 | }, 103 | }, 104 | OutputFormat: "", 105 | }, 106 | }, 107 | }, 108 | resources: []interface{}{ 109 | data, 110 | }, 111 | }, 112 | want: [][]string{ 113 | { 114 | "NAMESPACE", "NAME", "READY", "RESTARTS", "STATUS", "IP", "NODE", "AGE", 115 | }, 116 | { 117 | "default", "httpbin-74fb669cc6-8g9vf", "2/2", "33", "Running", "10.1.17.12", "docker-desktop", "145d", 118 | }, 119 | }, 120 | }, 121 | } 122 | for _, tt := range tests { 123 | t.Run(tt.name, func(t *testing.T) { 124 | got := GetResourceInTableFormat(tt.args.t, tt.args.resources) 125 | if arr := deep.Equal(got, tt.want); len(arr) > 0 { 126 | t.Errorf("GetResourceInTableFormat() = %v", arr) 127 | } 128 | }) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /internal/tui/actions.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rivo/tview" 7 | "github.com/sharadregoti/devops-plugin-sdk/proto" 8 | ) 9 | 10 | type Actions struct { 11 | view *tview.TextView 12 | nestingEnabled bool 13 | actions []*proto.Action 14 | } 15 | 16 | func NewAction() *Actions { 17 | t := tview.NewTextView() 18 | // t.SetBorder(true) 19 | t.SetTitle("Actions") 20 | 21 | return &Actions{ 22 | view: t, 23 | } 24 | } 25 | 26 | func (g *Actions) EnableNesting(v bool) { 27 | g.nestingEnabled = v 28 | } 29 | 30 | func (g *Actions) SetDefault(arr []*proto.Action) { 31 | g.actions = arr 32 | g.RefreshActions(arr) 33 | } 34 | 35 | func (g *Actions) ResetDefault() { 36 | g.RefreshActions(g.actions) 37 | } 38 | 39 | func (g *Actions) RefreshActions(arr []*proto.Action) { 40 | tempMap := map[string]string{} 41 | 42 | for _, a := range arr { 43 | tempMap[fmt.Sprintf("<%s>", a.KeyBinding)] = a.Name 44 | // tempMap[a.KeyBinding] = a.Name 45 | } 46 | 47 | // tempMap := map[string]string{"ctrl-y": "read", "ctrl-r": "refresh", "ctrl-a": "toggle search bar"} 48 | // if data.IsCreate { 49 | // tempMap["ctrl-c"] = "create" 50 | // } 51 | // if data.IsUpdate { 52 | // tempMap["ctrl-u"] = "update" 53 | // } 54 | // if data.IsDelete { 55 | // tempMap["ctrl-d"] = "delete" 56 | // } 57 | // g.view.SetText(createKeyValuePairsWithBrackets(tempMap)) 58 | g.view.SetText(getNiceFormat(tempMap)) 59 | 60 | } 61 | -------------------------------------------------------------------------------- /internal/tui/delete_modal_page.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import "github.com/rivo/tview" 4 | 5 | type deleteModalPage struct { 6 | view *tview.Modal 7 | } 8 | 9 | func newDeleteModalPage() *deleteModalPage { 10 | // TODO: Preselect no button 11 | modal := tview.NewModal(). 12 | SetText("Do you want to delete the resource?"). 13 | AddButtons([]string{"Yes", "No"}) 14 | 15 | return &deleteModalPage{ 16 | view: modal, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /internal/tui/eventloop.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import "github.com/sharadregoti/devops/model" 4 | 5 | func startInternalEventLoop(eventChan chan model.Event) { 6 | for event := range eventChan { 7 | switch event.Type { 8 | case string(model.ResourceTypeChanged), string(model.RefreshResource): 9 | 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /internal/tui/flash.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "github.com/gdamore/tcell/v2" 5 | "github.com/rivo/tview" 6 | ) 7 | 8 | type FlashView struct { 9 | view *tview.TextView 10 | } 11 | 12 | func NewFlashView() *FlashView { 13 | t := tview.NewTextView() 14 | t.SetTextColor(tcell.ColorYellow) 15 | 16 | v := &FlashView{ 17 | view: t, 18 | } 19 | 20 | return v 21 | } 22 | 23 | func (g *FlashView) GetView() *tview.TextView { 24 | return g.view 25 | } 26 | 27 | func (g *FlashView) SetText(text string) { 28 | g.view.SetText(text) 29 | } 30 | -------------------------------------------------------------------------------- /internal/tui/form_page.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import "github.com/rivo/tview" 4 | 5 | type formPage struct { 6 | view *tview.Form 7 | } 8 | 9 | func newFormPage() *formPage { 10 | genericUserInputFormBox := tview.NewForm() 11 | genericUserInputFormBox.SetBorder(true).SetTitle("Input Form") 12 | 13 | return &formPage{ 14 | view: genericUserInputFormBox, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internal/tui/general_info.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/rivo/tview" 8 | ) 9 | 10 | type GeneralInfo struct { 11 | view *tview.TextView 12 | } 13 | 14 | func NewGeneralInfo() *GeneralInfo { 15 | t := tview.NewTextView() 16 | // t.SetBorder(true) 17 | t.SetTitle("General Info") 18 | 19 | return &GeneralInfo{ 20 | view: t, 21 | } 22 | } 23 | 24 | func (g *GeneralInfo) GetView() *tview.TextView { 25 | return g.view 26 | } 27 | 28 | func (g *GeneralInfo) Refresh(data map[string]string) { 29 | g.view.Clear() 30 | // g.view.SetText(createKeyValuePairsWithoutBrackets(data)) 31 | g.view.SetText(getNiceFormat(data)) 32 | } 33 | 34 | func createKeyValuePairsWithoutBrackets(m map[string]string) string { 35 | b := new(bytes.Buffer) 36 | for key, value := range m { 37 | fmt.Fprintf(b, "%s: %s\n", key, value) 38 | } 39 | return b.String() 40 | } 41 | 42 | func createKeyValuePairsWithBrackets(m map[string]string) string { 43 | b := new(bytes.Buffer) 44 | for key, value := range m { 45 | fmt.Fprintf(b, "<%s> %s\n", key, value) 46 | } 47 | return b.String() 48 | } 49 | -------------------------------------------------------------------------------- /internal/tui/helpers.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // A function which return string for use.plugin 11 | const ( 12 | settingPlugin = "@use.plugin" 13 | settingAuthentication = "@use.auth" 14 | ) 15 | 16 | func getPluginSetting(name string) string { 17 | return fmt.Sprintf("%s.%s", settingPlugin, name) 18 | } 19 | 20 | // get plugin name from setting name using split function 21 | func parsePluginSetting(setting string) string { 22 | return strings.Split(setting, ".")[2] 23 | } 24 | 25 | // Returns identifyingName, name string 26 | func parseAuthenticationSetting(setting string) (string, string) { 27 | arr := strings.Split(setting, ".") 28 | return arr[2], arr[3] 29 | } 30 | 31 | func getAuthenticationSetting(identifyingName, name string) string { 32 | return fmt.Sprintf("%s.%s.%s", settingAuthentication, identifyingName, name) 33 | } 34 | 35 | // ChatGPT wrote this code, don't know how it works 36 | func getNiceFormat(data map[string]string) string { 37 | if len(data) == 0 { 38 | return "" 39 | } 40 | // data := map[string]string{ 41 | // "key1": "value1", 42 | // "key3": "value3", 43 | // "key2": "value2", 44 | // "key5": "value5", 45 | // "key4": "value4", 46 | // "key6": "value6", 47 | // "key7": "value7", 48 | // "key8": "value8", 49 | // "key9": "value9", 50 | // "key10": "value10", 51 | // "key11": "value11", 52 | // "key9434": "value9", 53 | // "key1012": "value10", 54 | // "key101266": "value10", 55 | // } 56 | 57 | var keys []string 58 | for k := range data { 59 | keys = append(keys, k) 60 | } 61 | sort.Strings(keys) 62 | 63 | var result string 64 | var columns [][]string 65 | var maxLengths []int 66 | for i, k := range keys { 67 | v := data[k] 68 | if i%5 == 0 { 69 | columns = append(columns, []string{}) 70 | maxLengths = append(maxLengths, len(k+": "+v)) 71 | } else { 72 | maxLengths[len(maxLengths)-1] = max(maxLengths[len(maxLengths)-1], len(k+": "+v)) 73 | } 74 | columns[len(columns)-1] = append(columns[len(columns)-1], k+": "+v) 75 | } 76 | 77 | transposed := transpose(columns) 78 | 79 | for i, row := range transposed { 80 | for j, cell := range row { 81 | if j > 0 { 82 | result += " " 83 | } 84 | if i == 0 { 85 | result += fmt.Sprintf("%-"+strconv.Itoa(maxLengths[j])+"s", cell) 86 | } else { 87 | parts := strings.SplitN(cell, ": ", 2) 88 | if len(parts) == 2 { 89 | key, value := parts[0], parts[1] 90 | if j < len(maxLengths)-1 { 91 | value = fmt.Sprintf("%-"+strconv.Itoa(maxLengths[j]-len(key)-2)+"s", value) 92 | } 93 | result += fmt.Sprintf("%s: %s", key, value) 94 | } 95 | } 96 | } 97 | result += "\n" 98 | } 99 | 100 | return strings.Trim(result, "\n") 101 | } 102 | 103 | func transpose(matrix [][]string) [][]string { 104 | numRows := len(matrix) 105 | numCols := len(matrix[0]) 106 | result := make([][]string, numCols) 107 | for i := 0; i < numCols; i++ { 108 | result[i] = make([]string, numRows) 109 | for j := 0; j < numRows; j++ { 110 | if i < len(matrix[j]) { 111 | result[i][j] = matrix[j][i] 112 | } else { 113 | result[i][j] = "" 114 | } 115 | } 116 | } 117 | return result 118 | } 119 | 120 | func max(a, b int) int { 121 | if a > b { 122 | return a 123 | } 124 | return b 125 | } 126 | -------------------------------------------------------------------------------- /internal/tui/isolator.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/rivo/tview" 8 | "github.com/sharadregoti/devops/utils/logger" 9 | ) 10 | 11 | type IsolatorView struct { 12 | view *tview.TextView 13 | // currentKeyMap map[string]string 14 | currentKeyMap []string 15 | // requiredIs []string 16 | } 17 | 18 | func NewIsolatorView() *IsolatorView { 19 | t := tview.NewTextView() 20 | // t.SetBorder(true) 21 | t.SetTitle("Isolator") 22 | 23 | v := &IsolatorView{ 24 | view: t, 25 | currentKeyMap: []string{}, 26 | // currentKeyMap: make(map[string]string), 27 | // requiredIs: []string{}, 28 | } 29 | 30 | // t.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 31 | // if event.Key() == tcell.KeyRune { 32 | // for i := range v.currentKeyMap { 33 | // fmt.Println("Here") 34 | // numToRune := fmt.Sprintf("%d", i)[0] 35 | // if event.Rune() == rune(numToRune) { 36 | // c <- model.Event{ 37 | // Type: model.IsolatorChanged, 38 | // IsolatorName: v.currentKeyMap[i], 39 | // } 40 | // } 41 | // } 42 | // } 43 | // return event 44 | // }) 45 | 46 | return v 47 | } 48 | 49 | func (g *IsolatorView) GetView() *tview.TextView { 50 | return g.view 51 | } 52 | 53 | func (g *IsolatorView) SetTitle(data string) { 54 | g.view.SetTitle(data) 55 | } 56 | 57 | func (g *IsolatorView) SetDefault(data []string) { 58 | g.view.Clear() 59 | // convert data to map 60 | // tempMap := make(map[string]string) 61 | // for i, v := range data { 62 | // tempMap[fmt.Sprintf("%d", i)] = v 63 | // } 64 | g.currentKeyMap = data 65 | // g.currentKeyMap = tempMap 66 | // g.requiredIs = data 67 | g.view.SetText(createKeyValuePairsIsolator(data)) 68 | } 69 | 70 | func (g *IsolatorView) AddAndRefreshView(isolatorName string) { 71 | if isolatorName == "" { 72 | return 73 | } 74 | 75 | // Don't add key if already exists 76 | for _, v := range g.currentKeyMap { 77 | if v == isolatorName { 78 | return 79 | } 80 | } 81 | 82 | // if len(g.currentKeyMap) == 4 { 83 | // // Remove the first element 84 | // index := len(g.requiredIs) 85 | // newMap := map[string]string{} 86 | 87 | // for k, v := range g.currentKeyMap { 88 | // i, _ := strconv.Atoi(k) 89 | // if i < index { 90 | // continue 91 | // } else if i == index { 92 | // newMap[fmt.Sprintf("%d", i)] = isolatorName 93 | // } else { 94 | // newMap[fmt.Sprintf("%d", i)] = v 95 | // } 96 | // } 97 | 98 | // g.currentKeyMap = newMap 99 | // } else { 100 | // g.currentKeyMap[fmt.Sprintf("%d", len(g.currentKeyMap))] = isolatorName 101 | // } 102 | 103 | // Insert the element at specific index, shift remaining by 1 104 | g.currentKeyMap = append(g.currentKeyMap[:1], append([]string{isolatorName}, g.currentKeyMap[1:]...)...) 105 | 106 | logger.LogDebug("Current Key Map: %v", len(g.currentKeyMap)) 107 | limit := 8 108 | if len(g.currentKeyMap) >= limit { 109 | // Cut off extra keys 110 | g.currentKeyMap = g.currentKeyMap[:limit-1] 111 | } 112 | 113 | tempMap := make(map[string]string) 114 | for i, v := range g.currentKeyMap { 115 | tempMap[fmt.Sprintf("%d", i)] = v 116 | } 117 | 118 | g.view.SetText(getNiceFormat(tempMap)) 119 | } 120 | 121 | func createKeyValuePairsIsolator(m []string) string { 122 | b := new(bytes.Buffer) 123 | for key, value := range m { 124 | fmt.Fprintf(b, "%d: %s\n", key, value) 125 | } 126 | return b.String() 127 | } 128 | -------------------------------------------------------------------------------- /internal/tui/main_page.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "github.com/rivo/tview" 5 | ) 6 | 7 | type mainPage struct { 8 | flexView *tview.Flex 9 | isolatorBox *IsolatorView 10 | generalInfoBox *GeneralInfo 11 | normalActionBox *Actions 12 | specificActionBox *SpecificActions 13 | tableBox *MainView 14 | searchBox *SearchView 15 | } 16 | 17 | func newMainPage() *mainPage { 18 | // Section1: Top level boxes that shows various info 19 | isolatorBox := NewIsolatorView() 20 | generalInfoBox := NewGeneralInfo() 21 | normalActionBox := NewAction() 22 | specificActionBox := NewSpecificAction() 23 | global := tview.NewFlex(). 24 | AddItem(generalInfoBox.GetView(), 0, 1, false). 25 | AddItem(isolatorBox.GetView(), 0, 1, false). 26 | AddItem(normalActionBox.view, 0, 1, false). 27 | AddItem(specificActionBox.GetView(), 0, 2, false) 28 | 29 | // Section2: Table 30 | tableBox := NewTableView() 31 | 32 | // Section3: Search 33 | searchBox := NewSearchView() 34 | 35 | rootFlexContainer := tview.NewFlex(). 36 | SetDirection(tview.FlexRow). 37 | AddItem(global, 5, 1, false). // Top level boxes that shows various info 38 | AddItem(tableBox.view, 0, 1, true). // Table 39 | AddItem(searchBox.GetView(), 0, 0, false) // Default disable 40 | 41 | return &mainPage{ 42 | flexView: rootFlexContainer, 43 | isolatorBox: isolatorBox, 44 | generalInfoBox: generalInfoBox, 45 | normalActionBox: normalActionBox, 46 | specificActionBox: specificActionBox, 47 | tableBox: tableBox, 48 | searchBox: searchBox, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/tui/plugin.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/sharadregoti/devops-plugin-sdk/proto" 7 | "github.com/sharadregoti/devops/model" 8 | "github.com/sharadregoti/devops/utils" 9 | "github.com/sharadregoti/devops/utils/logger" 10 | ) 11 | 12 | func (a *Application) connectAndLoadData(pluginName string, pluginAuth *proto.AuthInfo) error { 13 | // TODO: Show selection if default not found 14 | logger.LogDebug("Plugin auth (%s) (%s) (%s)", pluginName, pluginAuth.IdentifyingName, pluginAuth.Name) 15 | infoRes, err := a.getInfo(pluginName, pluginAuth.IdentifyingName, pluginAuth.Name) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | wsdata := make(chan model.WebsocketResponse, 1) 21 | 22 | if err := a.tableWebsocket(infoRes.SessionID, wsdata); err != nil { 23 | return err 24 | } 25 | 26 | // Initialize all boxes 27 | a.mainPage.normalActionBox.SetDefault(infoRes.Actions) 28 | a.mainPage.generalInfoBox.Refresh(infoRes.General) 29 | a.mainPage.isolatorBox.SetDefault(infoRes.DefaultIsolator) 30 | a.mainPage.isolatorBox.SetTitle(strings.Title(infoRes.IsolatorType)) 31 | a.mainPage.searchBox.SetResourceTypes(append(infoRes.ResourceTypes, a.settings...)) 32 | 33 | a.currentIsolator = infoRes.DefaultIsolator[0] 34 | a.currentResourceType = infoRes.IsolatorType 35 | a.sessionID = infoRes.SessionID 36 | a.wsdata = wsdata 37 | 38 | a.startGoRoutine() 39 | 40 | return nil 41 | } 42 | 43 | func (a *Application) startGoRoutine() { 44 | go func() { 45 | // a.mainPage.tableBox.view.dra 46 | logger.LogDebug("Started websocket data go routine for ID (%s)", a.sessionID) 47 | defer logger.LogDebug("Closing websocket data go routine for ID (%s)", a.sessionID) 48 | 49 | currentTableName := "" 50 | count := 0 51 | for { 52 | 53 | select { 54 | case <-a.closeChan: 55 | return 56 | 57 | case v := <-a.customTableChan: 58 | a.isCustomTableRenderingOn = true 59 | // a.mainPage.specificActionBox.RefreshActions(v.SpecificActions) 60 | // // c.appView.ActionView.EnableNesting(rs.currentSchema.Nesting.IsNested) 61 | a.mainPage.tableBox.SetTitle(v.TableName) 62 | a.mainPage.tableBox.Refresh(v.Data, 0) 63 | a.RemoveSearchView() 64 | // a.mainPage.tableBox.Refresh(v.Data, 0) 65 | a.application.SetFocus(a.mainPage.tableBox.view) 66 | a.application.Draw() 67 | 68 | case v := <-a.wsdata: 69 | 70 | if a.isCustomTableRenderingOn { 71 | // TODO: Store info of all table, so that when user comes back to normal table, we can show the same table 72 | continue 73 | } 74 | 75 | if currentTableName != strings.ToLower(v.TableName) { 76 | count = 0 77 | a.mainPage.tableBox.view.Clear() 78 | a.application.Draw() 79 | currentTableName = strings.ToLower(v.TableName) 80 | } 81 | 82 | // TODO: Optimize this 83 | headerRow := v.Data[0] 84 | a.mainPage.tableBox.SetHeader(headerRow) 85 | dataRow := v.Data[1] 86 | 87 | switch v.EventType { 88 | case "added": 89 | a.mainPage.tableBox.AddRow(v.Data[1:]) 90 | count++ 91 | case "modified", "updated": 92 | a.mainPage.tableBox.UpdateRow(v.Data[1:]) 93 | case "deleted": 94 | a.mainPage.tableBox.DeleteRow(dataRow) 95 | count-- 96 | default: 97 | logger.LogDebug("Unknown event type recieved from server (%s)", v.EventType) 98 | } 99 | 100 | a.mainPage.specificActionBox.RefreshActions(v.SpecificActions) 101 | // // c.appView.ActionView.EnableNesting(rs.currentSchema.Nesting.IsNested) 102 | a.mainPage.tableBox.SetTitle(utils.GetTableTitle(v.TableName, count)) 103 | a.RemoveSearchView() 104 | // a.mainPage.tableBox.Refresh(v.Data, 0) 105 | a.application.SetFocus(a.mainPage.tableBox.view) 106 | a.application.Draw() 107 | // a.mainPage.tableBox.view.Draw(tcell.NewSimulationScreen("UTF-8")) 108 | // a.application.Sync() 109 | logger.LogDebug("Websocket: received data from server, total length (%v)", len(v.Data)) 110 | } 111 | } 112 | }() 113 | 114 | } 115 | -------------------------------------------------------------------------------- /internal/tui/search.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gdamore/tcell/v2" 7 | "github.com/rivo/tview" 8 | "github.com/sahilm/fuzzy" 9 | ) 10 | 11 | type SearchView struct { 12 | view *tview.InputField 13 | } 14 | 15 | func NewSearchView() *SearchView { 16 | searchBox := tview.NewInputField() 17 | searchBox.SetFieldBackgroundColor(searchBox.GetBackgroundColor()) 18 | 19 | // searchBox.color 20 | searchBox.Autocomplete().SetDoneFunc(func(key tcell.Key) { 21 | // c <- model.Event{ 22 | // Type: string(model.ResourceTypeChanged), 23 | // ResourceType: searchBox.GetText(), 24 | // } 25 | // searchBox.SetText("") 26 | }) 27 | 28 | return &SearchView{ 29 | view: searchBox, 30 | } 31 | } 32 | 33 | func (s *SearchView) GetView() *tview.InputField { 34 | return s.view 35 | } 36 | 37 | func (s *SearchView) SetResourceTypes(arr []string) { 38 | s.view.SetAutocompleteFunc(func(currentText string) (entries []string) { 39 | if len(currentText) == 0 { 40 | return 41 | } 42 | 43 | matches := fuzzy.Matches{} 44 | // if strings.HasPrefix(currentText, "@") { 45 | // matches = fuzzy.Find(strings.ToLower(currentText), []string{"@default", "@xlr8s-dev"}) 46 | // } else { 47 | matches = fuzzy.Find(strings.ToLower(currentText), arr) 48 | // } 49 | 50 | for _, v := range matches { 51 | entries = append(entries, v.Str) 52 | } 53 | // for _, word := range arr { 54 | 55 | // if strings.HasPrefix(strings.ToLower(word), strings.ToLower(currentText)) { 56 | // entries = append(entries, word) 57 | // } 58 | // } 59 | if len(entries) == 0 { 60 | entries = nil 61 | } 62 | return 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /internal/tui/specific_actions.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rivo/tview" 7 | "github.com/sharadregoti/devops-plugin-sdk/proto" 8 | ) 9 | 10 | type SpecificActions struct { 11 | view *tview.TextView 12 | actions []*proto.Action 13 | } 14 | 15 | func NewSpecificAction() *SpecificActions { 16 | t := tview.NewTextView() 17 | // t.SetBorder(true) 18 | t.SetTitle("Specific Actions") 19 | 20 | return &SpecificActions{ 21 | view: t, 22 | } 23 | } 24 | 25 | func (g *SpecificActions) GetView() *tview.TextView { 26 | return g.view 27 | } 28 | 29 | func (g *SpecificActions) RefreshActions(data []*proto.Action) { 30 | temp := map[string]string{} 31 | for _, sa := range data { 32 | temp[fmt.Sprintf("<%s>", sa.KeyBinding)] = sa.Name 33 | } 34 | // g.view.SetText(createKeyValuePairsWithBrackets(temp)) 35 | // TODO: This can be expensive 36 | g.view.SetText(getNiceFormat(temp)) 37 | g.actions = data 38 | } 39 | -------------------------------------------------------------------------------- /internal/tui/start.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sharadregoti/devops/utils/logger" 7 | ) 8 | 9 | func Start(address string) error { 10 | logger.InitClientLogging() 11 | 12 | logger.Loggero.Info("You can find the logs at: ~/.devops/devops-tui.log") 13 | 14 | logger.LogDebug("Starting application...") 15 | app, err := NewApplication(address) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | logger.LogDebug("Getting app config from server...") 21 | appConfig, err := app.getAppConfig() 22 | if err != nil { 23 | return err 24 | } 25 | app.appConfig = appConfig 26 | 27 | // Throw error if no plugins are configured 28 | if len(appConfig.Plugins) == 0 { 29 | return fmt.Errorf("no plugins configured") 30 | } 31 | 32 | logger.LogDebug("Loading data...") 33 | if err := app.loadPlugin(appConfig.Plugins[0].Name); err != nil { 34 | return fmt.Errorf("failed to load data: %v", err) 35 | } 36 | 37 | if err := app.Start(); err != nil { 38 | return fmt.Errorf("failed to start application: %v", err) 39 | } 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /internal/tui/table.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "github.com/gdamore/tcell/v2" 5 | "github.com/rivo/tview" 6 | "github.com/sharadregoti/devops/model" 7 | "github.com/sharadregoti/devops/utils/logger" 8 | "golang.org/x/text/cases" 9 | "golang.org/x/text/language" 10 | ) 11 | 12 | type MainView struct { 13 | view *tview.Table 14 | } 15 | 16 | func NewTableView() *MainView { 17 | table := tview.NewTable().SetFixed(0, 0) 18 | table.SetBorder(true).SetBorderAttributes(tcell.AttrDim).SetTitle("Table") 19 | table.SetSelectable(true, false) 20 | 21 | // table.Select(0, 0).SetSelectedFunc(func(row int, column int) { 22 | // if row == 0 { 23 | // return 24 | // } 25 | // c <- model.Event{ 26 | // Type: model.ReadResource, 27 | // RowIndex: row, 28 | // } 29 | // }) 30 | 31 | return &MainView{ 32 | view: table, 33 | } 34 | } 35 | 36 | func (m *MainView) SetTitle(title string) { 37 | m.view.SetTitle(cases.Title(language.AmericanEnglish).String(title)) 38 | } 39 | 40 | func (m *MainView) Refresh(data []*model.TableRow, rowNum int) { 41 | m.view.Clear() 42 | 43 | for r, cols := range data { 44 | for c, col := range cols.Data { 45 | // Set header 46 | if r < 1 { 47 | m.SetHeaderCell(r, c, col) 48 | continue 49 | } 50 | 51 | m.SetCell(r, c, col, getColor(cols.Color)) 52 | } 53 | } 54 | m.view.Select(rowNum, 0) 55 | } 56 | 57 | func (m *MainView) GetRowNum(id string) int { 58 | for r := 0; r < m.view.GetRowCount(); r++ { 59 | // 1 is always the id column 60 | if m.view.GetCell(r, 1).Text == id { 61 | return r 62 | } 63 | } 64 | return -1 65 | } 66 | 67 | func (m *MainView) SetHeader(d *model.TableRow) { 68 | for i, colValue := range d.Data { 69 | m.SetCell(0, i, colValue, getColor("white")) 70 | } 71 | } 72 | 73 | func (m *MainView) AddRow(d []*model.TableRow) { 74 | currentRowCount := m.view.GetRowCount() 75 | for rowNumber, row := range d { 76 | for i, colValue := range row.Data { 77 | m.SetCell(currentRowCount+rowNumber, i, colValue, getColor(row.Color)) 78 | } 79 | } 80 | } 81 | 82 | func (m *MainView) UpdateRow(d []*model.TableRow) { 83 | for _, rowInfo := range d { 84 | row := m.GetRowNum(rowInfo.Data[1]) 85 | if row <= 0 { 86 | // logger.LogDebug("Update row, row not found for id: (%s)", rowInfo.Data[1]) 87 | // logger.LogDebug("Hi, here %v", rowInfo.Data) 88 | m.AddRow(d) 89 | return 90 | } 91 | // logger.LogDebug("Updating row number (%d) with id (%s)", row, rowInfo.Data[1]) 92 | for i, colValue := range rowInfo.Data { 93 | m.SetCell(row, i, colValue, getColor(rowInfo.Color)) 94 | } 95 | } 96 | } 97 | 98 | func (m *MainView) DeleteRow(d *model.TableRow) { 99 | row := m.GetRowNum(d.Data[1]) 100 | if row <= 0 { 101 | logger.LogDebug("Delete row, row not found for id: (%s)", d.Data[1]) 102 | return 103 | } 104 | m.view.RemoveRow(row) 105 | } 106 | 107 | func (m *MainView) SetHeaderCell(x, y int, text string) { 108 | m.view.SetCell(x, y, 109 | tview.NewTableCell(text). 110 | SetTextColor(tcell.ColorWhite). 111 | SetAlign(tview.AlignLeft).SetExpansion(1)) 112 | } 113 | 114 | func (m *MainView) SetCell(x, y int, text string, color tcell.Color) { 115 | m.view.SetCell(x, y, 116 | tview.NewTableCell(text). 117 | SetTextColor(color). 118 | SetAlign(tview.AlignLeft).SetExpansion(1)) 119 | } 120 | 121 | func getColor(color string) tcell.Color { 122 | 123 | switch color { 124 | case "darkorange": 125 | return tcell.ColorDarkOrange 126 | case "gray": 127 | return tcell.ColorGray 128 | case "white": 129 | return tcell.ColorWhite 130 | case "lightskyblue": 131 | return tcell.ColorLightSkyBlue 132 | case "mediumpurple": 133 | return tcell.ColorMediumPurple 134 | case "red": 135 | return tcell.ColorRed 136 | case "yellow": 137 | return tcell.ColorYellow 138 | case "blue": 139 | return tcell.ColorBlue 140 | case "orange": 141 | return tcell.ColorOrange 142 | case "green": 143 | return tcell.ColorGreen 144 | case "aqua": 145 | return tcell.ColorAqua 146 | default: 147 | return tcell.ColorWhite 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /internal/tui/text_only_page.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "github.com/rivo/tview" 5 | ) 6 | 7 | type textOnlyPage struct { 8 | view *tview.TextView 9 | } 10 | 11 | func newTextOnlyPage() *textOnlyPage { 12 | textOnlyBox := tview.NewTextView() 13 | textOnlyBox.SetDynamicColors(true) 14 | 15 | return &textOnlyPage{ 16 | view: textOnlyBox, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /internal/tui/utils.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sharadregoti/devops/utils/logger" 7 | ) 8 | 9 | func (a *Application) flashLogError(msg string, args ...interface{}) error { 10 | str := fmt.Sprintf(msg, args...) 11 | a.SetFlashText(str) 12 | logger.LogError("%v", str) 13 | return fmt.Errorf("%v", str) 14 | } 15 | -------------------------------------------------------------------------------- /model/config.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/ghodss/yaml" 10 | ) 11 | 12 | // Config stores app config 13 | type Config struct { 14 | Server *Server `json:"server" yaml:"server" binding:"required"` 15 | Plugins []*Plugin `json:"plugins" yaml:"plugins" binding:"required"` 16 | } 17 | 18 | type Server struct { 19 | Address string `json:"address" yaml:"address" binding:"required"` 20 | } 21 | 22 | type Plugin struct { 23 | Name string `json:"name" yaml:"name" binding:"required"` 24 | IsDefault bool `json:"isDefault" yaml:"isDefault" binding:"required"` 25 | } 26 | 27 | // Creates .devops directory if it does not exists 28 | func InitCoreDirectory() string { 29 | // Get the user's home directory 30 | homeDir, err := os.UserHomeDir() 31 | if err != nil { 32 | log.Fatal("Cannot detect home directory", err) 33 | } 34 | 35 | // Create the ".devops" subdirectory if it doesn't exist 36 | devopsDir := filepath.Join(homeDir, ".devops") 37 | if _, err := os.Stat(devopsDir); os.IsNotExist(err) { 38 | err = os.Mkdir(devopsDir, 0755) 39 | if err != nil { 40 | log.Fatal("Cannot create .devops directory", err) 41 | } 42 | } else if !os.IsExist(err) && err != nil { 43 | log.Fatal("Cannot get stats of directory", err) 44 | } 45 | 46 | return devopsDir 47 | } 48 | 49 | func LoadConfig(devopsDir string) *Config { 50 | fmt.Println("Loading config file...") 51 | c := new(Config) 52 | configBytes, err := os.ReadFile(filepath.Join(devopsDir, "config.yaml")) 53 | if os.IsNotExist(err) { 54 | // Load default 55 | fmt.Println("config.yaml not found, loading default configuration") 56 | c = &Config{ 57 | Plugins: []*Plugin{ 58 | { 59 | Name: "kubernetes", 60 | }, 61 | // { 62 | // Name: "aws", 63 | // }, 64 | }, 65 | } 66 | } else if !os.IsExist(err) && err != nil { 67 | log.Fatal("failed to read config.yaml file", err) 68 | } 69 | 70 | if err := yaml.Unmarshal(configBytes, c); err != nil { 71 | log.Fatal("failed to yaml unmarshal config file", err) 72 | } 73 | return c 74 | } 75 | -------------------------------------------------------------------------------- /model/events.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Event struct { 4 | Type string 5 | // TODO: Remove this as not required 6 | RowIndex int 7 | 8 | // Resource 9 | ResourceName string 10 | ResourceType string 11 | 12 | // Isolator 13 | IsolatorName string 14 | 15 | // Specific Action 16 | // TODO: Remove this as not required 17 | SpecificActionName string 18 | 19 | // Plugin 20 | PluginName string 21 | 22 | Args map[string]interface{} 23 | // Args map[string]*_struct.Value 24 | } 25 | 26 | // type EventType[T NormalEvent | InternalEvent | SpecficEvent] string 27 | 28 | type EventType string 29 | type NormalEvent string 30 | type InternalEvent string 31 | type SpecficEvent string 32 | type OutputType string 33 | 34 | const ( 35 | OutputTypeEvent string = "event" 36 | OutputTypeString string = "string" 37 | OutputTypeNothing string = "nothing" 38 | OutputTypeStream string = "stream" 39 | OutputTypeBidrectional string = "bidirectional" 40 | ) 41 | 42 | const ( 43 | NormalAction EventType = "normal-action" 44 | InternalAction EventType = "internal-action" 45 | SpecificAction EventType = "specfic-action" 46 | ) 47 | 48 | const ( 49 | // Generic Actions 50 | // ReadResource event show entire json/yaml of a resource in full screen view 51 | // Required fields: RowIndex 52 | ReadResource NormalEvent = "read" 53 | // DeleteResource event shows a modal promt for deleting a resource 54 | // Required fields: ResourceName, ResourceType, IsolatorName 55 | DeleteResource = "delete" 56 | UpdateResource = "update" 57 | CreateResource = "create" 58 | EditResource NormalEvent = "edit" 59 | ViewLongRunning NormalEvent = "view-long-running" 60 | DeleteLongRunning NormalEvent = "delete-long-running" 61 | 62 | // ShowModal event shows a modal promt 63 | // Required fields: ResourceName, ResourceType, IsolatorName 64 | ShowModal = "show-delete-modal" 65 | 66 | // Resource 67 | // Required fields 68 | ResourceTypeChanged InternalEvent = "resource-type-change" 69 | RefreshResource InternalEvent = "refresh-resource" 70 | CloseEventLoop InternalEvent = "closer-event-loop" 71 | 72 | // Stream 73 | Close InternalEvent = "close" 74 | 75 | // Isolator 76 | // AddIsolator SpecficEvent = "add-isolator" 77 | IsolatorChanged NormalEvent = "isolator-change" 78 | 79 | // Specific Action 80 | SpecificActionOccured SpecficEvent = "specific-action-occured" 81 | 82 | SpecificActionResolveArgs SpecficEvent = "specific-action-resolve-args" 83 | 84 | ViewNestedResource SpecficEvent = "view-nested-resource" 85 | 86 | // Plugin 87 | PluginChanged = "plugin-change" 88 | 89 | NestBack = "nest-back" 90 | ) 91 | -------------------------------------------------------------------------------- /model/http.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "os/exec" 5 | 6 | "github.com/gorilla/websocket" 7 | "github.com/sharadregoti/devops-plugin-sdk/proto" 8 | ) 9 | 10 | type WebsocketReadWriter struct { 11 | Socket *websocket.Conn 12 | } 13 | 14 | func NewSocketReadWrite(conn *websocket.Conn) *WebsocketReadWriter { 15 | return &WebsocketReadWriter{ 16 | Socket: conn, 17 | } 18 | } 19 | 20 | func (srw WebsocketReadWriter) Write(p []byte) (n int, err error) { 21 | err = srw.Socket.WriteMessage(websocket.TextMessage, p) 22 | return len(p), err 23 | } 24 | 25 | func (srw WebsocketReadWriter) Read(p []byte) (n int, err error) { 26 | _, b, err := srw.Socket.ReadMessage() 27 | for i, d := range b { 28 | p[i] = d 29 | } 30 | return len(b), err 31 | } 32 | 33 | type InfoResponse struct { 34 | SessionID string `json:"id" yaml:"id" binding:"required"` 35 | General map[string]string `json:"general" yaml:"general" binding:"required"` 36 | Actions []*proto.Action `json:"actions" yaml:"actions" binding:"required"` 37 | ResourceTypes []string `json:"resourceTypes" yaml:"resourceTypes" binding:"required"` 38 | DefaultIsolator []string `json:"defaultIsolator" yaml:"defaultIsolator" binding:"required"` 39 | IsolatorType string `json:"isolatorType" yaml:"isolatorType" binding:"required"` 40 | } 41 | 42 | // type Action struct { 43 | // // Type can be one of normal, special, internal 44 | // Type EventType `json:"type" yaml:"type"` 45 | // // Name is the name to be shown on UI 46 | // Name string `json:"name" yaml:"name"` 47 | // KeyBinding string `json:"key_binding" yaml:"key_binding"` 48 | // // Output type can be 49 | // OutputType OutputType `json:"output_type" yaml:"output_type"` 50 | // } 51 | 52 | type FrontendEvent struct { 53 | EventType EventType `json:"eventType" yaml:"eventType"` 54 | ActionName string `json:"name" yaml:"name" enums:"read,delete,update,create,edit,view-long-running,delete-long-running,resource-type-change,isolator-change,refresh-resource"` 55 | ResourceType string `json:"resourceType" yaml:"resourceType"` 56 | ResourceName string `json:"resourceName" yaml:"resourceName"` 57 | IsolatorName string `json:"isolatorName" yaml:"isolatorName"` 58 | PluginName string `json:"pluginName" yaml:"pluginName"` 59 | Args map[string]interface{} `json:"args" yaml:"args"` 60 | } 61 | 62 | type WebsocketResponse struct { 63 | ID string `json:"id" yaml:"id"` 64 | TableName string `json:"name" yaml:"name"` 65 | EventType string `json:"eventType" yaml:"eventType"` 66 | Data []*TableRow `json:"data" yaml:"data"` 67 | SpecificActions []*proto.Action `json:"specificActions" yaml:"specificActions"` 68 | } 69 | 70 | type ErrorResponse struct { 71 | Message string `json:"message" yaml:"message"` 72 | } 73 | 74 | type EventResponse struct { 75 | ID string `json:"id" yaml:"id"` 76 | Result interface{} `json:"result" yaml:"result"` 77 | } 78 | 79 | type LongRunningInfo struct { 80 | ID string `json:"id"` 81 | Name string `json:"name"` 82 | Status string `json:"status"` 83 | Message string `json:"message"` 84 | e *Event 85 | cmd *exec.Cmd 86 | } 87 | 88 | func (lri *LongRunningInfo) SetE(e *Event) { 89 | lri.e = e 90 | } 91 | 92 | func (lri *LongRunningInfo) GetE() *Event { 93 | return lri.e 94 | } 95 | 96 | func (lri *LongRunningInfo) SetCMD(e *exec.Cmd) { 97 | lri.cmd = e 98 | } 99 | 100 | func (lri *LongRunningInfo) GetCMD() *exec.Cmd { 101 | return lri.cmd 102 | } 103 | 104 | type AuthResponse struct { 105 | Auths []*proto.AuthInfo `json:"auths" yaml:"auths"` 106 | } 107 | -------------------------------------------------------------------------------- /model/transformer.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type TableRow struct { 4 | Data []string `json:"data" yaml:"data"` 5 | Color string `json:"color" yaml:"color"` 6 | } 7 | -------------------------------------------------------------------------------- /plugins/helm/.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | builds: 5 | - id: devops-helm-plugin 6 | env: 7 | - CGO_ENABLED=0 8 | flags: 9 | - -tags=release 10 | # - -o helm 11 | main: . 12 | binary: helm 13 | goos: 14 | - linux 15 | # - windows 16 | - darwin 17 | release: 18 | disable: true 19 | skip_upload: true 20 | 21 | archives: 22 | - builds: 23 | - devops-helm-plugin 24 | id: devops-helm-plugin 25 | name_template: "devops-helm-plugin_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 26 | files: 27 | - config.yaml 28 | - src: resource_config 29 | dst: resource_config 30 | strip_parent: true 31 | replacements: 32 | # darwin: Darwin 33 | # linux: Linux 34 | # windows: Windows 35 | 386: i386 36 | amd64: x86_64 37 | checksum: 38 | name_template: "checksums.txt" 39 | snapshot: 40 | name_template: "{{ incpatch .Version }}-next" 41 | changelog: 42 | sort: asc 43 | filters: 44 | exclude: 45 | - "^docs:" 46 | - "^test:" 47 | 48 | blobs: 49 | - provider: gs 50 | bucket: devops-cli-artifacts 51 | folder: "releases/devops/{{.Version}}" 52 | ids: 53 | - devops-helm-plugin 54 | -------------------------------------------------------------------------------- /plugins/helm/config.yaml: -------------------------------------------------------------------------------- 1 | # # Example configuration 2 | kube_configs: 3 | - name: aws-dev-cluster 4 | path: "/home/sharad/aws/dev2.kubeconfig" 5 | contexts: 6 | - name: "default" 7 | default_namespaces_to_show: 8 | - "default" 9 | - "dev-aws" 10 | -------------------------------------------------------------------------------- /plugins/helm/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/hashicorp/go-hclog" 7 | "github.com/hashicorp/go-plugin" 8 | 9 | shared "github.com/sharadregoti/devops-plugin-sdk" 10 | ) 11 | 12 | // handshakeConfigs are used to just do a basic handshake between 13 | // a plugin and host. If the handshake fails, a user friendly error is shown. 14 | // This prevents users from executing bad plugins or executing a plugin 15 | // directory. It is a UX feature, not a security feature. 16 | var handshakeConfig = plugin.HandshakeConfig{ 17 | ProtocolVersion: 1, 18 | MagicCookieKey: "BASIC_PLUGIN", 19 | MagicCookieValue: "hello", 20 | } 21 | 22 | func main() { 23 | logger := hclog.New(&hclog.LoggerOptions{ 24 | Name: "helm", 25 | Level: shared.GetHCLLogLevel(), 26 | Output: os.Stderr, 27 | }) 28 | shared.Logger = logger 29 | 30 | shared.LogInfo("Starting helm plugin server") 31 | 32 | pluginK8s, err := New(logger.Named(PluginName)) 33 | if err != nil { 34 | shared.LogError("failed to initialized helm plugin: %v", err) 35 | } 36 | 37 | // pluginMap is the map of plugins we can dispense. 38 | var pluginMap = map[string]plugin.Plugin{ 39 | "helm": &shared.DevopsPlugin{Impl: pluginK8s, Logger: logger}, 40 | } 41 | 42 | plugin.Serve(&plugin.ServeConfig{ 43 | HandshakeConfig: handshakeConfig, 44 | Plugins: pluginMap, 45 | Logger: logger, 46 | GRPCServer: plugin.DefaultGRPCServer, 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /plugins/helm/release.go: -------------------------------------------------------------------------------- 1 | //go:build release 2 | // +build release 3 | 4 | package main 5 | 6 | func init() { 7 | release = true 8 | } 9 | -------------------------------------------------------------------------------- /plugins/helm/resource_config/charts.yaml: -------------------------------------------------------------------------------- 1 | # For a chart (helm show) 2 | # You can show what resources it will create. 3 | # View chart info 4 | # View readme 5 | # Show default values 6 | # Custom CRDs 7 | operations: 8 | - name: "namespace" 9 | json_paths: 10 | - path: "" 11 | output_format: "" 12 | - name: "name" 13 | json_paths: 14 | - path: "" 15 | output_format: "" 16 | - name: "url" 17 | json_paths: 18 | - path: "" 19 | output_format: "" 20 | styles: 21 | - row_background_color: lightskyblue 22 | conditions: 23 | - 'true' 24 | specific_actions: -------------------------------------------------------------------------------- /plugins/helm/resource_config/defaults.yaml: -------------------------------------------------------------------------------- 1 | operations: 2 | - name: "namespace" 3 | json_paths: 4 | - path: "metadata.namespace" 5 | output_format: "" 6 | - name: "name" 7 | json_paths: 8 | - path: "metadata.name" 9 | output_format: "" 10 | - name: "status" 11 | json_paths: 12 | - path: "status.phase" 13 | output_format: "" 14 | - name: "age" 15 | json_paths: 16 | - path: "metadata.creationTimestamp|@age" 17 | output_format: "" 18 | styles: 19 | - row_background_color: mediumpurple 20 | conditions: 21 | - 'status.phase == "Terminating"' 22 | - row_background_color: lightskyblue 23 | conditions: 24 | - 'status.phase == "Active"' 25 | - row_background_color: lightskyblue 26 | conditions: 27 | - 'status.phase == "NA"' 28 | specific_actions: 29 | - name: "describe" 30 | key_binding: "d" 31 | execution: 32 | cmd: | 33 | #!/bin/bash 34 | kubectl describe {{.resourceType}} {{.resourceName}} -n {{.isolatorName}} --kubeconfig {{.authPath}} --context {{.authName}} 35 | output_type: "string" -------------------------------------------------------------------------------- /plugins/helm/resource_config/releases.yaml: -------------------------------------------------------------------------------- 1 | # For a release (helm get) 2 | # You can list release (helm list -n namespace) 3 | # You can see values (helm get ) 4 | # You can do rollbacks (helm rollback ) 5 | # You can see release (helm hisotry -n namesapce release name) 6 | operations: 7 | - name: "namespace" 8 | json_paths: 9 | - path: "namespace" 10 | output_format: "" 11 | - name: "name" 12 | json_paths: 13 | - path: "name" 14 | output_format: "" 15 | - name: "revision" 16 | json_paths: 17 | - path: "revision" 18 | output_format: "" 19 | - name: "updated" 20 | json_paths: 21 | - path: "updated" 22 | output_format: "" 23 | - name: "status" 24 | json_paths: 25 | - path: "status" 26 | output_format: "" 27 | - name: "chart" 28 | json_paths: 29 | - path: "chart" 30 | output_format: "" 31 | - name: "app_version" 32 | json_paths: 33 | - path: "app_version" 34 | output_format: "" 35 | styles: 36 | - row_background_color: lightskyblue 37 | conditions: 38 | - 'true' 39 | specific_actions: 40 | - name: "show hooks" 41 | key_binding: "o" 42 | execution: 43 | cmd: | 44 | #!/bin/bash 45 | helm get hooks {{.resourceName}} -n {{.isolatorName}} --kubeconfig {{.authPath}} --kube-context {{.authName}} 46 | output_type: "string" 47 | - name: "show manifest" 48 | key_binding: "m" 49 | execution: 50 | cmd: | 51 | #!/bin/bash 52 | helm get manifest {{.resourceName}} -n {{.isolatorName}} --kubeconfig {{.authPath}} --kube-context {{.authName}} 53 | output_type: "string" 54 | - name: "show values" 55 | key_binding: "v" 56 | execution: 57 | cmd: | 58 | #!/bin/bash 59 | helm get values {{.resourceName}} -n {{.isolatorName}} --kubeconfig {{.authPath}} --kube-context {{.authName}} 60 | output_type: "string" 61 | - name: "show history" 62 | key_binding: "h" 63 | execution: 64 | cmd: | 65 | #!/bin/bash 66 | helm history {{.resourceName}} -n {{.isolatorName}} --kubeconfig {{.authPath}} --kube-context {{.authName}} 67 | output_type: "string" 68 | - name: "rollback" 69 | key_binding: "r" 70 | execution: 71 | cmd: | 72 | #!/bin/bash 73 | helm rollback {{.resourceName}} -n {{.isolatorName}} --kubeconfig {{.authPath}} --kube-context {{.authName}} 74 | output_type: "string" 75 | - name: "uninstall" 76 | key_binding: "u" 77 | execution: 78 | cmd: | 79 | #!/bin/bash 80 | helm uninstall {{.resourceName}} -n {{.isolatorName}} --kubeconfig {{.authPath}} --kube-context {{.authName}} 81 | output_type: "string" -------------------------------------------------------------------------------- /plugins/helm/resource_config/repos.yaml: -------------------------------------------------------------------------------- 1 | # Remove repo 2 | # Update repo 3 | operations: 4 | - name: "name" 5 | json_paths: 6 | - path: "name" 7 | output_format: "" 8 | - name: "url" 9 | json_paths: 10 | - path: "url" 11 | output_format: "" 12 | styles: 13 | - row_background_color: lightskyblue 14 | conditions: 15 | - 'true' 16 | specific_actions: -------------------------------------------------------------------------------- /plugins/kubernetes/.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | builds: 5 | - id: devops-kubernetes-plugin 6 | env: 7 | - CGO_ENABLED=0 8 | flags: 9 | - -tags=release 10 | # - -o kubernetes 11 | main: . 12 | binary: kubernetes 13 | goos: 14 | - linux 15 | # - windows 16 | - darwin 17 | release: 18 | disable: true 19 | skip_upload: true 20 | 21 | archives: 22 | - builds: 23 | - devops-kubernetes-plugin 24 | id: devops-kubernetes-plugin 25 | name_template: "devops-kubernetes-plugin_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 26 | files: 27 | - config.yaml 28 | - src: resource_config 29 | dst: resource_config 30 | strip_parent: true 31 | replacements: 32 | # darwin: Darwin 33 | # linux: Linux 34 | # windows: Windows 35 | 386: i386 36 | amd64: x86_64 37 | checksum: 38 | name_template: 'checksums.txt' 39 | snapshot: 40 | name_template: "{{ incpatch .Version }}-next" 41 | changelog: 42 | sort: asc 43 | filters: 44 | exclude: 45 | - '^docs:' 46 | - '^test:' 47 | 48 | blobs: 49 | - provider: gs 50 | bucket: devops-cli-artifacts 51 | folder: "releases/devops/{{.Version}}" 52 | ids: 53 | - devops-kubernetes-plugin 54 | -------------------------------------------------------------------------------- /plugins/kubernetes/config.yaml: -------------------------------------------------------------------------------- 1 | # Example configuration 2 | kube_configs: 3 | - name: aws-dev-cluster 4 | path: "/home/sharad/aws/dev2.kubeconfig" 5 | contexts: 6 | - name: "default" 7 | default_namespaces_to_show: 8 | - "default" 9 | - "dev-aws" 10 | -------------------------------------------------------------------------------- /plugins/kubernetes/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sharadregoti/devops-kubernetes-plugin 2 | 3 | go 1.19 4 | 5 | replace github.com/sharadregoti/devops-plugin-sdk => ../../devops-plugin-sdk 6 | 7 | require ( 8 | github.com/hashicorp/go-hclog v1.4.0 9 | github.com/sharadregoti/devops-plugin-sdk v0.0.0-00010101000000-000000000000 10 | k8s.io/apimachinery v0.26.1 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/emicklei/go-restful/v3 v3.9.0 // indirect 16 | github.com/go-logr/logr v1.2.3 // indirect 17 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 18 | github.com/go-openapi/jsonreference v0.20.0 // indirect 19 | github.com/go-openapi/swag v0.19.14 // indirect 20 | github.com/gogo/protobuf v1.3.2 // indirect 21 | github.com/golang/protobuf v1.5.2 // indirect 22 | github.com/google/gnostic v0.5.7-v3refs // indirect 23 | github.com/google/go-cmp v0.5.9 // indirect 24 | github.com/google/gofuzz v1.1.0 // indirect 25 | github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect 26 | github.com/imdario/mergo v0.3.6 // indirect 27 | github.com/josharian/intern v1.0.0 // indirect 28 | github.com/json-iterator/go v1.1.12 // indirect 29 | github.com/mailru/easyjson v0.7.6 // indirect 30 | github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 // indirect 31 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 32 | github.com/modern-go/reflect2 v1.0.2 // indirect 33 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 34 | github.com/oklog/run v1.0.0 // indirect 35 | github.com/spf13/pflag v1.0.5 // indirect 36 | github.com/tidwall/match v1.1.1 // indirect 37 | github.com/tidwall/pretty v1.2.0 // indirect 38 | golang.org/x/net v0.6.0 // indirect 39 | golang.org/x/oauth2 v0.4.0 // indirect 40 | golang.org/x/term v0.5.0 // indirect 41 | golang.org/x/text v0.7.0 // indirect 42 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect 43 | google.golang.org/appengine v1.6.7 // indirect 44 | google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc // indirect 45 | google.golang.org/grpc v1.53.0 // indirect 46 | google.golang.org/protobuf v1.28.1 // indirect 47 | gopkg.in/inf.v0 v0.9.1 // indirect 48 | gopkg.in/yaml.v2 v2.4.0 // indirect 49 | gopkg.in/yaml.v3 v3.0.1 // indirect 50 | k8s.io/klog/v2 v2.80.1 // indirect 51 | k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect 52 | k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect 53 | sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect 54 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 55 | sigs.k8s.io/yaml v1.3.0 // indirect 56 | ) 57 | 58 | require ( 59 | github.com/fatih/color v1.13.0 // indirect 60 | github.com/ghodss/yaml v1.0.0 61 | github.com/hashicorp/go-plugin v1.4.8 62 | github.com/mattn/go-colorable v0.1.12 // indirect 63 | github.com/mattn/go-isatty v0.0.14 // indirect 64 | github.com/tidwall/gjson v1.14.4 65 | golang.org/x/sys v0.5.0 // indirect 66 | k8s.io/api v0.26.1 67 | k8s.io/client-go v0.26.1 68 | ) 69 | -------------------------------------------------------------------------------- /plugins/kubernetes/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/hashicorp/go-hclog" 7 | "github.com/hashicorp/go-plugin" 8 | 9 | shared "github.com/sharadregoti/devops-plugin-sdk" 10 | ) 11 | 12 | // handshakeConfigs are used to just do a basic handshake between 13 | // a plugin and host. If the handshake fails, a user friendly error is shown. 14 | // This prevents users from executing bad plugins or executing a plugin 15 | // directory. It is a UX feature, not a security feature. 16 | var handshakeConfig = plugin.HandshakeConfig{ 17 | ProtocolVersion: 1, 18 | MagicCookieKey: "BASIC_PLUGIN", 19 | MagicCookieValue: "hello", 20 | } 21 | 22 | func main() { 23 | logger := hclog.New(&hclog.LoggerOptions{ 24 | Name: "k8s", 25 | Level: shared.GetHCLLogLevel(), 26 | Output: os.Stderr, 27 | }) 28 | shared.Logger = logger 29 | 30 | shared.LogInfo("Starting kubernetes plugin server") 31 | 32 | pluginK8s, err := New(logger.Named(PluginName)) 33 | if err != nil { 34 | shared.LogError("failed to initialized kubernetes plugin: %v", err) 35 | } 36 | 37 | // pluginMap is the map of plugins we can dispense. 38 | var pluginMap = map[string]plugin.Plugin{ 39 | "kubernetes": &shared.DevopsPlugin{Impl: pluginK8s, Logger: logger}, 40 | } 41 | 42 | plugin.Serve(&plugin.ServeConfig{ 43 | HandshakeConfig: handshakeConfig, 44 | Plugins: pluginMap, 45 | Logger: logger, 46 | GRPCServer: plugin.DefaultGRPCServer, 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /plugins/kubernetes/release.go: -------------------------------------------------------------------------------- 1 | //go:build release 2 | // +build release 3 | 4 | package main 5 | 6 | func init() { 7 | release = true 8 | } 9 | -------------------------------------------------------------------------------- /plugins/kubernetes/resource_config/configmaps.yaml: -------------------------------------------------------------------------------- 1 | operations: 2 | - name: "namespace" 3 | json_paths: 4 | - path: "metadata.namespace" 5 | output_format: "" 6 | - name: "name" 7 | json_paths: 8 | - path: "metadata.name" 9 | output_format: "" 10 | - name: "data" 11 | json_paths: 12 | - path: "data.#" 13 | output_format: "" 14 | - name: "age" 15 | json_paths: 16 | - path: "metadata.creationTimestamp|@age" 17 | output_format: "" 18 | styles: 19 | - row_background_color: lightskyblue 20 | conditions: 21 | - "true" 22 | specific_actions: 23 | - name: "describe" 24 | key_binding: "d" 25 | execution: 26 | cmd: | 27 | #!/bin/bash 28 | kubectl describe {{.resourceType}} {{.resourceName}} -n "{{.isolatorName}}" --kubeconfig {{.authPath}} --context {{.authName}} 29 | output_type: "string" 30 | -------------------------------------------------------------------------------- /plugins/kubernetes/resource_config/containers.yaml: -------------------------------------------------------------------------------- 1 | operations: 2 | - name: "namespace" 3 | json_paths: 4 | - path: "metadata.namespace" 5 | output_format: "" 6 | - name: "name" 7 | json_paths: 8 | - path: "name" 9 | output_format: "" 10 | - name: "image" 11 | json_paths: 12 | - path: "image" 13 | output_format: "" 14 | - name: "status" 15 | json_paths: 16 | - path: "status.phase" 17 | output_format: "" 18 | - name: "age" 19 | json_paths: 20 | - path: "metadata.creationTimestamp" 21 | output_format: "" 22 | nesting: 23 | # is_nested: true 24 | # resource_type: "containers" 25 | # args: 26 | is_self_contained_in_parent: true 27 | parent_data_paths : 28 | - spec.containers 29 | - spec.initContainers 30 | specific_actions: 31 | - name: "logs" 32 | key_binding: "l" 33 | scrren_action: "view" 34 | output_type: "stream" 35 | args: 36 | parentName: "metadata.name" 37 | parentResourceType: "pods" -------------------------------------------------------------------------------- /plugins/kubernetes/resource_config/cronjobs.yaml: -------------------------------------------------------------------------------- 1 | operations: 2 | - name: "namespace" 3 | json_paths: 4 | - path: "metadata.namespace" 5 | output_format: "" 6 | - name: "name" 7 | json_paths: 8 | - path: "metadata.name" 9 | output_format: "" 10 | - name: "status" 11 | json_paths: 12 | - path: "status.phase" 13 | output_format: "" 14 | - name: "age" 15 | json_paths: 16 | - path: "metadata.creationTimestamp|@age" 17 | output_format: "" 18 | nesting: 19 | is_nested: true 20 | resource_type: "pods" 21 | args: 22 | labels: "spec.selector.matchLabels" 23 | styles: 24 | - row_background_color: lightskyblue 25 | conditions: 26 | - "true" 27 | specific_actions: 28 | - name: "describe" 29 | key_binding: "d" 30 | execution: 31 | cmd: | 32 | #!/bin/bash 33 | kubectl describe {{.resourceType}} {{.resourceName}} -n "{{.isolatorName}}" --kubeconfig {{.authPath}} --context {{.authName}} 34 | output_type: "string" 35 | -------------------------------------------------------------------------------- /plugins/kubernetes/resource_config/daemonsets.yaml: -------------------------------------------------------------------------------- 1 | operations: 2 | - name: "namespace" 3 | json_paths: 4 | - path: "metadata.namespace" 5 | output_format: "" 6 | - name: "name" 7 | json_paths: 8 | - path: "metadata.name" 9 | output_format: "" 10 | - name: "status" 11 | json_paths: 12 | - path: "status.phase" 13 | output_format: "" 14 | - name: "age" 15 | json_paths: 16 | - path: "metadata.creationTimestamp|@age" 17 | output_format: "" 18 | nesting: 19 | is_nested: true 20 | resource_type: "pods" 21 | args: 22 | labels: "spec.selector.matchLabels" 23 | styles: 24 | - row_background_color: lightskyblue 25 | conditions: 26 | - "true" 27 | specific_actions: 28 | - name: "describe" 29 | key_binding: "d" 30 | execution: 31 | cmd: | 32 | #!/bin/bash 33 | kubectl describe {{.resourceType}} {{.resourceName}} -n "{{.isolatorName}}" --kubeconfig {{.authPath}} --context {{.authName}} 34 | output_type: "string" 35 | -------------------------------------------------------------------------------- /plugins/kubernetes/resource_config/defaults.yaml: -------------------------------------------------------------------------------- 1 | operations: 2 | - name: "namespace" 3 | json_paths: 4 | - path: "metadata.namespace" 5 | output_format: "" 6 | - name: "name" 7 | json_paths: 8 | - path: "metadata.name" 9 | output_format: "" 10 | - name: "status" 11 | json_paths: 12 | - path: "status.phase" 13 | output_format: "" 14 | - name: "age" 15 | json_paths: 16 | - path: "metadata.creationTimestamp|@age" 17 | output_format: "" 18 | styles: 19 | - row_background_color: mediumpurple 20 | conditions: 21 | - 'status.phase == "Terminating"' 22 | - row_background_color: lightskyblue 23 | conditions: 24 | - 'status.phase == "Active"' 25 | - row_background_color: lightskyblue 26 | conditions: 27 | - 'status.phase == "NA"' 28 | specific_actions: 29 | - name: "describe" 30 | key_binding: "d" 31 | execution: 32 | cmd: | 33 | #!/bin/bash 34 | kubectl describe {{.resourceType}} {{.resourceName}} -n "{{.isolatorName}}" --kubeconfig {{.authPath}} --context {{.authName}} 35 | output_type: "string" 36 | -------------------------------------------------------------------------------- /plugins/kubernetes/resource_config/deployments.yaml: -------------------------------------------------------------------------------- 1 | operations: 2 | - name: "namespace" 3 | json_paths: 4 | - path: "metadata.namespace" 5 | output_format: "" 6 | - name: "name" 7 | json_paths: 8 | - path: "metadata.name" 9 | output_format: "" 10 | - name: "ready" 11 | json_paths: 12 | - path: "status.readyReplicas" 13 | - path: "status.replicas" 14 | output_format: "%v/%v" 15 | - name: "up-to-date" 16 | json_paths: 17 | - path: "status.readyReplicas" 18 | output_format: "" 19 | - name: "available" 20 | json_paths: 21 | - path: "status.availableReplicas" 22 | output_format: "" 23 | - name: "age" 24 | json_paths: 25 | - path: "metadata.creationTimestamp|@age" 26 | output_format: "" 27 | nesting: 28 | is_nested: true 29 | resource_type: "pods" 30 | args: 31 | labels: "spec.selector.matchLabels" 32 | styles: 33 | - row_background_color: lightskyblue 34 | conditions: 35 | - "true" 36 | specific_actions: 37 | - name: "describe" 38 | key_binding: "d" 39 | execution: 40 | cmd: | 41 | #!/bin/bash 42 | kubectl describe {{.resourceType}} {{.resourceName}} -n "{{.isolatorName}}" --kubeconfig {{.authPath}} --context {{.authName}} 43 | output_type: "string" 44 | -------------------------------------------------------------------------------- /plugins/kubernetes/resource_config/jobs.yaml: -------------------------------------------------------------------------------- 1 | operations: 2 | - name: "namespace" 3 | json_paths: 4 | - path: "metadata.namespace" 5 | output_format: "" 6 | - name: "name" 7 | json_paths: 8 | - path: "metadata.name" 9 | output_format: "" 10 | - name: "status" 11 | json_paths: 12 | - path: "status.phase" 13 | output_format: "" 14 | - name: "age" 15 | json_paths: 16 | - path: "metadata.creationTimestamp|@age" 17 | output_format: "" 18 | nesting: 19 | is_nested: true 20 | resource_type: "pods" 21 | args: 22 | labels: "spec.selector.matchLabels" 23 | styles: 24 | - row_background_color: lightskyblue 25 | conditions: 26 | - "true" 27 | specific_actions: 28 | - name: "describe" 29 | key_binding: "d" 30 | execution: 31 | cmd: | 32 | #!/bin/bash 33 | kubectl describe {{.resourceType}} {{.resourceName}} -n "{{.isolatorName}}" --kubeconfig {{.authPath}} --context {{.authName}} 34 | output_type: "string" 35 | -------------------------------------------------------------------------------- /plugins/kubernetes/resource_config/namespaces.yaml: -------------------------------------------------------------------------------- 1 | operations: 2 | - name: "namespace" 3 | json_paths: 4 | - path: "metadata.namespace" 5 | output_format: "" 6 | - name: "name" 7 | json_paths: 8 | - path: "metadata.name" 9 | output_format: "" 10 | - name: "status" 11 | json_paths: 12 | - path: "status.phase" 13 | output_format: "" 14 | - name: "age" 15 | json_paths: 16 | - path: "metadata.creationTimestamp|@age" 17 | output_format: "" 18 | styles: 19 | - row_background_color: mediumpurple 20 | conditions: 21 | - 'status.phase == "Terminating"' 22 | - row_background_color: lightskyblue 23 | conditions: 24 | - 'status.phase == "Active"' 25 | - row_background_color: lightskyblue 26 | conditions: 27 | - 'status.phase == "NA"' 28 | specific_actions: 29 | - name: "describe" 30 | key_binding: "d" 31 | execution: 32 | cmd: | 33 | #!/bin/bash 34 | kubectl describe {{.resourceType}} {{.resourceName}} -n "{{.isolatorName}}" --kubeconfig {{.authPath}} --context {{.authName}} 35 | output_type: "string" 36 | # - name: "get all" 37 | # key_binding: "g" 38 | # execution: 39 | # cmd: | 40 | # #!/bin/bash 41 | # kubectl get-all --namespace={{.resourceName}} --kubeconfig {{.authPath}} --context {{.authName}} 42 | # output_type: "string" 43 | -------------------------------------------------------------------------------- /plugins/kubernetes/resource_config/nodes.yaml: -------------------------------------------------------------------------------- 1 | operations: 2 | - name: "namespace" 3 | json_paths: 4 | - path: "metadata.namespace" 5 | output_format: "" 6 | - name: "name" 7 | json_paths: 8 | - path: "metadata.name" 9 | output_format: "" 10 | - name: "status" 11 | json_paths: 12 | - path: "devops.customCalculatedStatus" 13 | output_format: "" 14 | - name: "version" 15 | json_paths: 16 | - path: "status.nodeInfo.kubeletVersion" 17 | output_format: "" 18 | - name: "pods" 19 | json_paths: 20 | - path: "status.capacity.na" 21 | - path: "status.capacity.pods" 22 | output_format: "%v/%v" 23 | - name: "age" 24 | json_paths: 25 | - path: "metadata.creationTimestamp|@age" 26 | output_format: "" 27 | styles: 28 | - row_background_color: darkorange 29 | conditions: 30 | - 'devops.customCalculatedStatus == "SchedulingDisabled" || devops.customCalculatedStatus == "Ready,SchedulingDisabled" || devops.customCalculatedStatus == "Not Ready,SchedulingDisabled"' 31 | - row_background_color: mediumpurple 32 | conditions: 33 | - 'devops.customCalculatedStatus == "Terminating"' 34 | - row_background_color: lightskyblue 35 | conditions: 36 | - 'devops.customCalculatedStatus == "Ready"' 37 | - row_background_color: red 38 | conditions: 39 | - "true" 40 | specific_actions: 41 | - name: "describe" 42 | key_binding: "d" 43 | execution: 44 | cmd: | 45 | #!/bin/bash 46 | kubectl describe {{.resourceType}} {{.resourceName}} -n "{{.isolatorName}}" --kubeconfig {{.authPath}} --context {{.authName}} 47 | output_type: "string" 48 | - name: "cordon" 49 | key_binding: "c" 50 | execution: 51 | cmd: | 52 | #!/bin/bash 53 | kubectl cordon {{.resourceName}} --kubeconfig {{.authPath}} --context {{.authName}} 54 | output_type: "nothing" 55 | - name: "uncordon" 56 | key_binding: "c" 57 | execution: 58 | cmd: | 59 | #!/bin/bash 60 | kubectl uncordon {{.resourceName}} --kubeconfig {{.authPath}} --context {{.authName}} 61 | output_type: "nothing" 62 | - name: "drain" 63 | key_binding: "c" 64 | execution: 65 | cmd: | 66 | #!/bin/bash 67 | kubectl drain {{.resourceName}} --delete-emptydir-data={{.args.deleteLocalData}} --force={{.args.force}} --grace-period={{.args.gracePeriod}} --ignore-daemonsets={{.args.ignoreDaemonSets}} --kubeconfig {{.authPath}} --context {{.authName}} 68 | user_input: 69 | required: true 70 | args: 71 | # The below template returns the first container port of the first container of the pod 72 | gracePeriod: "-1" 73 | ignoreDaemonSets: "false" 74 | deleteLocalData: "false" 75 | force: "false" 76 | server_input: 77 | required: false 78 | output_type: "string" 79 | -------------------------------------------------------------------------------- /plugins/kubernetes/resource_config/replicasets.yaml: -------------------------------------------------------------------------------- 1 | geoperations: 2 | - name: "namespace" 3 | json_paths: 4 | - path: "metadata.namespace" 5 | output_format: "" 6 | - name: "name" 7 | json_paths: 8 | - path: "metadata.name" 9 | output_format: "" 10 | - name: "desired" 11 | json_paths: 12 | - path: "status.replicas" 13 | output_format: "%v" 14 | - name: "current" 15 | json_paths: 16 | - path: "status.availableReplicas" 17 | output_format: "" 18 | - name: "ready" 19 | json_paths: 20 | - path: "status.readyReplicas" 21 | output_format: "" 22 | - name: "age" 23 | json_paths: 24 | - path: "metadata.creationTimestamp|@age" 25 | output_format: "" 26 | nesting: 27 | is_nested: true 28 | resource_type: "pods" 29 | args: 30 | labels: "spec.selector.matchLabels" 31 | styles: 32 | - row_background_color: lightskyblue 33 | conditions: 34 | - "true" 35 | specific_actions: 36 | - name: "describe" 37 | key_binding: "d" 38 | execution: 39 | cmd: | 40 | #!/bin/bash 41 | kubectl describe {{.resourceType}} {{.resourceName}} -n "{{.isolatorName}}" --kubeconfig {{.authPath}} --context {{.authName}} 42 | output_type: "string" 43 | -------------------------------------------------------------------------------- /plugins/kubernetes/resource_config/secrets.yaml: -------------------------------------------------------------------------------- 1 | operations: 2 | - name: "namespace" 3 | json_paths: 4 | - path: "metadata.namespace" 5 | output_format: "" 6 | - name: "name" 7 | json_paths: 8 | - path: "metadata.name" 9 | output_format: "" 10 | - name: "type" 11 | json_paths: 12 | - path: "type" 13 | output_format: "" 14 | - name: "age" 15 | json_paths: 16 | - path: "metadata.creationTimestamp|@age" 17 | output_format: "" 18 | styles: 19 | - row_background_color: lightskyblue 20 | conditions: 21 | - "true" 22 | specific_actions: 23 | - name: "describe" 24 | key_binding: "d" 25 | execution: 26 | cmd: | 27 | #!/bin/bash 28 | kubectl describe {{.resourceType}} {{.resourceName}} -n "{{.isolatorName}}" --kubeconfig {{.authPath}} --context {{.authName}} 29 | output_type: "string" 30 | - name: "decode-secret" 31 | key_binding: "x" 32 | execution: 33 | cmd: | 34 | #!/bin/bash 35 | 36 | # use kubectl to get the secret data in JSON format and extract the Base64-encoded values 37 | secret_data=$(kubectl get {{.resourceType}} {{.resourceName}} -n {{.isolatorName}} --kubeconfig {{.authPath}} --context {{.authName}} -o json | jq -r '.data') 38 | 39 | # iterate over the keys in the secret data and decode the values 40 | for key in $(echo "${secret_data}" | jq -r 'keys[]'); do 41 | value=$(echo "${secret_data}" | jq -r --arg key "$key" '.[$key]' | base64 -d) 42 | echo "${key}: ${value}" 43 | done 44 | output_type: "string" 45 | -------------------------------------------------------------------------------- /plugins/kubernetes/resource_config/serviceaccounts.yaml: -------------------------------------------------------------------------------- 1 | operations: 2 | - name: "namespace" 3 | json_paths: 4 | - path: "metadata.namespace" 5 | output_format: "" 6 | - name: "name" 7 | json_paths: 8 | - path: "metadata.name" 9 | output_format: "" 10 | - name: "status" 11 | json_paths: 12 | - path: "status.phase" 13 | output_format: "" 14 | - name: "age" 15 | json_paths: 16 | - path: "metadata.creationTimestamp|@age" 17 | output_format: "" 18 | styles: 19 | - row_background_color: mediumpurple 20 | conditions: 21 | - 'status.phase == "Terminating"' 22 | - row_background_color: lightskyblue 23 | conditions: 24 | - 'status.phase == "Active"' 25 | - row_background_color: lightskyblue 26 | conditions: 27 | - 'status.phase == "NA"' 28 | specific_actions: 29 | - name: "describe" 30 | key_binding: "d" 31 | execution: 32 | cmd: | 33 | #!/bin/bash 34 | kubectl describe {{.resourceType}} {{.resourceName}} -n "{{.isolatorName}}" --kubeconfig {{.authPath}} --context {{.authName}} 35 | output_type: "string" 36 | # - name: "check access" 37 | # key_binding: "c" 38 | # execution: 39 | # cmd: | 40 | # #!/bin/bash 41 | # kubectl access-matrix --sa {{.isolatorName}}:{{.resourceName}} --kubeconfig {{.authPath}} --context {{.authName}} 42 | # output_type: "string" 43 | -------------------------------------------------------------------------------- /plugins/kubernetes/resource_config/services.yaml: -------------------------------------------------------------------------------- 1 | operations: 2 | - name: "namespace" 3 | json_paths: 4 | - path: "metadata.namespace" 5 | output_format: "" 6 | - name: "name" 7 | json_paths: 8 | - path: "metadata.name" 9 | output_format: "" 10 | - name: "type" 11 | json_paths: 12 | - path: "spec.type" 13 | output_format: "" 14 | - name: "cluster-ip" 15 | json_paths: 16 | - path: "spec.clusterIPs" 17 | output_format: "" 18 | - name: "external-ip" 19 | json_paths: 20 | - path: "status.loadBalancer.ingress" 21 | output_format: "" 22 | - name: "ports" 23 | json_paths: 24 | - path: "spec.ports|@flatten|@pick:name,nodePort,port,targetPort" 25 | output_format: "" 26 | - name: "age" 27 | json_paths: 28 | - path: "metadata.creationTimestamp|@age" 29 | output_format: "" 30 | nesting: 31 | is_nested: true 32 | resource_type: "pods" 33 | args: 34 | labels: "spec.selector" 35 | styles: 36 | - row_background_color: lightskyblue 37 | conditions: 38 | - "true" 39 | specific_actions: 40 | - name: "describe" 41 | key_binding: "d" 42 | execution: 43 | cmd: | 44 | #!/bin/bash 45 | kubectl describe {{.resourceType}} {{.resourceName}} -n "{{.isolatorName}}" --kubeconfig {{.authPath}} --context {{.authName}} 46 | output_type: "string" 47 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Devops CLI: A tool to do DevOps in style 2 | 3 | A Terminal/Web UI to interact with devops tools & services, intended for devops/platform engineers. 4 | 5 | ## Inspiration 6 | This project is inspired from the following softwares terraform, K9s, Lens. 7 | - Devops CLI aims to bring the agility (that extensively comes from using keyboard) & speed offered by K9s. 8 | - Devops CLI aims to bring the extensibility provided by terraform (in form of plugins) 9 | - Not everything can be done on TUI, Something are better show in a web app like Lens. The above 2 things will also be package in a Web UI for better experience. 10 | 11 | ## This project aims to achieve the following: 12 | - Improve debugging 13 | - Improve development agility 14 | 15 | ## [Watch Web App Demo 1](https://youtu.be/DtQnuSDmodg) 16 | ## [Watch Web App Demo 2 (new ux)](https://youtu.be/0nEwPfzeikQ) 17 | 18 | ## Installation 19 | **Linux & Mac** 20 | 21 | `curl https://storage.googleapis.com/devops-cli-artifacts/releases/devops/0.5.3/install.sh | bash` 22 | 23 | ## Usage 24 | 25 | **Run Server** 26 | 27 | `devops` 28 | 29 | **Run Client (Web App)** 30 | 31 | ![image](https://user-images.githubusercontent.com/24411676/230721653-a57f0eea-7629-4839-ba32-1eb6cb77415f.png) 32 | 33 | 34 | `On browser go to: http://localhost:9753` 35 | 36 | **Run Client (TUI)** 37 | 38 | `devops tui` 39 | 40 | ## Read [Wiki](https://github.com/sharadregoti/devops-cli/wiki) for detailed documentation 41 | 42 | ## Supported Plugins 43 | - Kubernetes 44 | - Helm 45 | - Gitlab (WIP) 46 | 47 | ### Kubernetes Features 48 | - View & search all cluster resources 49 | - Create, Read, Update, Delete any resource 50 | - Describe any resource 51 | - View logs, get shell access of pod 52 | 53 | ### Helm Features 54 | - View releases & charts 55 | - Perform the following operation on releases: rollback, uninstall, view values, view history, view manifies etc... 56 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # echo "Did you uncomment the production URL (y/n)?" 4 | # read answer 5 | 6 | # if [ "$answer" != "${answer#[Nn]}" ] ;then 7 | # echo "Exitting..." 8 | # exit 0 9 | # fi 10 | 11 | # echo "Did increment version number in code (y/n)?" 12 | # read answer 13 | 14 | # if [ "$answer" != "${answer#[Nn]}" ] ;then 15 | # echo "Exitting..." 16 | # exit 0 17 | # fi 18 | 19 | # echo "Did create a new git tag as per current version number (y/n)?" 20 | # read answer 21 | 22 | # if [ "$answer" != "${answer#[Nn]}" ] ;then 23 | # echo "Exitting..." 24 | # exit 0 25 | # fi 26 | 27 | # echo "Building frontend..." 28 | cd devops-frontend 29 | # Build frontend 30 | yarn 31 | yarn build 32 | # Create tar file 33 | tar -czvf ui.tar.gz dist/ 34 | 35 | # Build Core Binary 36 | echo "Building core binary..." 37 | cd ../ 38 | goreleaser release --clean --skip-validate 39 | 40 | # Build kubernetes plugins 41 | cd plugins/kubernetes 42 | goreleaser release --clean --skip-validate 43 | 44 | cd ../ 45 | 46 | # Build kubernetes plugins 47 | cd helm 48 | goreleaser release --clean --skip-validate 49 | -------------------------------------------------------------------------------- /server/middleware.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | // import ( 4 | // "bytes" 5 | // "io" 6 | // "net/http" 7 | 8 | // "github.com/Azure/go-autorest/logger" 9 | // ) 10 | 11 | // // LoggerMiddleWare logs all incomming request 12 | // func LoggerMiddleWare(next http.Handler) http.Handler { 13 | // return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 14 | // var reqBody []byte 15 | // if r.Header.Get("Content-Type") == "application/json" { 16 | // reqBody, _ = io.ReadAll(r.Body) 17 | // r.Body = io.NopCloser(bytes.NewBuffer(reqBody)) 18 | // } 19 | 20 | // logger.LogInfo("Request", map[string]interface{}{"method": r.Method, "url": r.URL.Path, "queryVars": r.URL.Query(), "body": string(reqBody)}) 21 | // next.ServeHTTP(w, r) 22 | // }) 23 | // } 24 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | middleware "github.com/gorilla/handlers" 8 | "github.com/gorilla/mux" 9 | pm "github.com/sharadregoti/devops/internal/pluginmanager" 10 | "github.com/sharadregoti/devops/model" 11 | "github.com/sharadregoti/devops/server/handlers" 12 | "github.com/sharadregoti/devops/utils" 13 | "github.com/sharadregoti/devops/utils/logger" 14 | ) 15 | 16 | // Server is the object which sets up the server and handles all server operations 17 | type Server struct { 18 | Sm *pm.SessionManager 19 | config *model.Config 20 | } 21 | 22 | func New(conf *model.Config) (*Server, error) { 23 | sm, err := pm.NewSM(conf) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return &Server{ 29 | Sm: sm, 30 | config: conf, 31 | }, nil 32 | } 33 | 34 | type Data struct { 35 | TableName string 36 | Table [][]string 37 | } 38 | 39 | func (s *Server) routes() http.Handler { 40 | router := mux.NewRouter() 41 | 42 | router.Methods(http.MethodGet).Path("/v1/config").HandlerFunc(handlers.HandleConfig(s.config)) 43 | router.Methods(http.MethodGet).Path("/v1/auth/{pluginName}").HandlerFunc(handlers.HandleAuth(s.Sm)) 44 | // https://github.com/gorilla/mux/issues/77#issuecomment-522849160 45 | router.Methods(http.MethodGet).Path("/v1/connect/{pluginName}/{authId}/{contextId:.+}").HandlerFunc(handlers.HandleInfo(s.Sm)) 46 | router.Methods(http.MethodPost).Path("/v1/events/{id}").HandlerFunc(handlers.HandleEvent(s.Sm)) 47 | router.Methods(http.MethodGet).Path("/v1/ws/{id}").HandlerFunc(handlers.HandleWebsocket(s.Sm)) 48 | router.Methods(http.MethodGet).Path("/v1/ws/action/{clientId}/{id}").HandlerFunc(handlers.HandleActionWebsocket(s.Sm)) 49 | router.PathPrefix("/").Handler(http.FileServer(http.Dir(model.InitCoreDirectory() + "/dist"))) 50 | 51 | return router 52 | } 53 | 54 | func (s *Server) Start() error { 55 | fmt.Println("Starting server on port:", s.config.Server.Address) 56 | fmt.Printf("You can visit the app at : http://%s\n", s.config.Server.Address) 57 | return http.ListenAndServe(s.config.Server.Address, utils.CreateCorsObject().Handler(middleware.LoggingHandler(logger.GetFileWriter(), s.routes()))) 58 | } 59 | -------------------------------------------------------------------------------- /utils/grpc.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | structpb "github.com/golang/protobuf/ptypes/struct" 5 | structpbt "google.golang.org/protobuf/types/known/structpb" 6 | ) 7 | 8 | func GetMap(data map[string]interface{}) map[string]*structpb.Value { 9 | m := make(map[string]*structpb.Value) 10 | for k, v := range data { 11 | newVal, err := structpbt.NewValue(v) 12 | if err != nil { 13 | return nil 14 | } 15 | m[k] = newVal 16 | } 17 | 18 | return m 19 | } 20 | 21 | func GetMapInterface(data map[string]*structpb.Value) map[string]interface{} { 22 | m := make(map[string]interface{}) 23 | for k, v := range data { 24 | m[k] = v.AsInterface() 25 | } 26 | 27 | return m 28 | } 29 | -------------------------------------------------------------------------------- /utils/http.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "strings" 10 | 11 | // "github.com/go-errors/errors" 12 | "github.com/rs/cors" 13 | "github.com/sharadregoti/devops/model" 14 | ) 15 | 16 | // CreateCorsObject creates a cors object with the required config 17 | func CreateCorsObject() *cors.Cors { 18 | return cors.New(cors.Options{ 19 | AllowCredentials: true, 20 | AllowOriginFunc: func(s string) bool { 21 | return true 22 | }, 23 | AllowedMethods: []string{"GET", "PUT", "POST", "DELETE"}, 24 | AllowedHeaders: []string{"Authorization", "Content-Type"}, 25 | ExposedHeaders: []string{"Authorization", "Content-Type"}, 26 | }) 27 | } 28 | 29 | // CloseTheCloser closes the closer 30 | func CloseTheCloser(c io.Closer) { 31 | _ = c.Close() 32 | } 33 | 34 | // SendResponse sends an http response 35 | func SendResponse(ctx context.Context, w http.ResponseWriter, statusCode int, body interface{}) error { 36 | w.Header().Set("Cache-Control", "no-store") 37 | w.Header().Set("Content-Type", "application/json") 38 | w.WriteHeader(statusCode) 39 | 40 | return json.NewEncoder(w).Encode(body) 41 | } 42 | 43 | // SendErrorResponse sends an http error response 44 | func SendErrorResponse(ctx context.Context, w http.ResponseWriter, statusCode int, err error) error { 45 | w.Header().Set("Cache-Control", "no-store") 46 | w.Header().Set("Content-Type", "application/json") 47 | w.WriteHeader(statusCode) 48 | 49 | fmt.Println("Errror resposne 1 status has been sent") 50 | response := model.ErrorResponse{ 51 | Message: err.Error(), 52 | } 53 | 54 | fmt.Println("Errror resposne 2 message bing sent") 55 | return json.NewEncoder(w).Encode(response) 56 | } 57 | 58 | // ExtractToken extracts token from http request 59 | func ExtractToken(r *http.Request) string { 60 | bearToken := r.Header.Get("Authorization") 61 | strArr := strings.Split(bearToken, " ") 62 | if len(strArr) == 2 { 63 | return strArr[1] 64 | } else if len(strArr) == 1 { 65 | return strArr[0] 66 | } 67 | return "" 68 | } 69 | -------------------------------------------------------------------------------- /utils/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/hashicorp/go-hclog" 11 | shared "github.com/sharadregoti/devops-plugin-sdk" 12 | "github.com/sharadregoti/devops/model" 13 | ) 14 | 15 | var Loggerf hclog.Logger 16 | var Loggero hclog.Logger 17 | var fileWriter io.Writer 18 | 19 | func init() { 20 | // Init plugins 21 | devopsDir := model.InitCoreDirectory() 22 | filePath := filepath.Join(devopsDir, "devops.log") 23 | file, err := getCoreLogFile(filePath) 24 | if err != nil { 25 | log.Fatal("Error while creating log file", err) 26 | return 27 | } 28 | 29 | fileWriter = file 30 | Loggero, Loggerf = createLoggers(file) 31 | } 32 | 33 | func InitClientLogging() { 34 | devopsDir := model.InitCoreDirectory() 35 | filePath := filepath.Join(devopsDir, "devops-tui.log") 36 | file, err := getCoreLogFile(filePath) 37 | if err != nil { 38 | log.Fatal("Error while creating log file", err) 39 | return 40 | } 41 | 42 | fileWriter = file 43 | Loggero, Loggerf = createLoggers(file) 44 | } 45 | 46 | func createLoggers(file *os.File) (loggero, loggerf hclog.Logger) { 47 | loggero = hclog.New(&hclog.LoggerOptions{ 48 | Name: "devops", 49 | Output: os.Stdout, 50 | Level: shared.GetHCLLogLevel(), 51 | }) 52 | 53 | loggerf = hclog.New(&hclog.LoggerOptions{ 54 | Name: "devops", 55 | Output: file, 56 | Level: shared.GetHCLLogLevel(), 57 | }) 58 | 59 | return 60 | } 61 | 62 | func GetFileWriter() io.Writer { 63 | return fileWriter 64 | } 65 | 66 | func getCoreLogFile(filePath string) (*os.File, error) { 67 | // Check if the file exists 68 | _, err := os.Stat(filePath) 69 | if os.IsNotExist(err) { 70 | // If file does not exist, create it 71 | file, err := os.Create(filePath) 72 | if err != nil { 73 | return nil, err 74 | } 75 | return file, nil 76 | } 77 | 78 | // If file exists, open it 79 | file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) 80 | if err != nil { 81 | return nil, err 82 | } 83 | return file, nil 84 | } 85 | 86 | func LogInfo(msg string, args ...interface{}) { 87 | if len(args) > 0 { 88 | Loggerf.Info(fmt.Sprintf(msg, args...)) 89 | } else { 90 | Loggerf.Info(msg) 91 | } 92 | } 93 | 94 | func LogDebug(msg string, args ...interface{}) { 95 | if len(args) > 0 { 96 | Loggerf.Debug(fmt.Sprintf(msg, args...)) 97 | } else { 98 | Loggerf.Debug(msg) 99 | } 100 | } 101 | 102 | func LogTrace(msg string, args ...interface{}) { 103 | if len(args) > 0 { 104 | Loggerf.Trace(fmt.Sprintf(msg, args...)) 105 | } else { 106 | Loggerf.Trace(msg) 107 | } 108 | } 109 | 110 | func LogError(msg string, args ...interface{}) error { 111 | if len(args) > 0 { 112 | Loggerf.Error(fmt.Sprintf(msg, args...)) 113 | return fmt.Errorf(fmt.Sprintf(msg, args...)) 114 | } else { 115 | Loggerf.Error(msg) 116 | return fmt.Errorf(msg) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /utils/template.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "html/template" 5 | "strings" 6 | 7 | "github.com/Masterminds/sprig" 8 | "github.com/sharadregoti/devops/utils/logger" 9 | "gopkg.in/yaml.v2" 10 | ) 11 | 12 | // ExecuteTemplate applies a parsed template to the specified data object 13 | func ExecuteTemplate(tmpl string, params interface{}) (string, error) { 14 | funcMap := map[string]interface{}{ 15 | "toYaml": func(val interface{}) string { 16 | data, _ := yaml.Marshal(val) 17 | return string(data) 18 | }, 19 | "tpl": func(name string, data interface{}) string { 20 | tmpl, _ := template.New(name).Parse(name) 21 | var result strings.Builder 22 | if err := tmpl.Execute(&result, data); err != nil { 23 | logger.LogError("failed to execute tpl function inside template: %v", err) 24 | return "" 25 | } 26 | return result.String() 27 | }, 28 | } 29 | 30 | t, err := template.New("tmpl").Funcs(funcMap).Funcs(sprig.GenericFuncMap()).Parse(tmpl) 31 | if err != nil { 32 | return "", logger.LogError("Failed to parse template: %v", err) 33 | } 34 | 35 | var b strings.Builder 36 | if err := t.Execute(&b, params); err != nil { 37 | return "", logger.LogError("Failed to execute template with given parameters: %v", err) 38 | } 39 | return b.String(), nil 40 | } 41 | -------------------------------------------------------------------------------- /validator/docker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/docker/docker/api/types" 9 | "github.com/docker/docker/client" 10 | ) 11 | 12 | func imageCheck(imageName string) bool { 13 | // Create a new Docker client 14 | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) 15 | if err != nil { 16 | fmt.Println(err) 17 | os.Exit(1) 18 | } 19 | 20 | // Search for the image by name 21 | images, err := cli.ImageList(context.Background(), types.ImageListOptions{All: true}) 22 | if err != nil { 23 | fmt.Println(err) 24 | os.Exit(1) 25 | } 26 | 27 | // Check if the image exists 28 | exists := false 29 | for _, image := range images { 30 | for _, tag := range image.RepoTags { 31 | if tag == imageName { 32 | exists = true 33 | break 34 | } 35 | } 36 | } 37 | 38 | if exists { 39 | return true 40 | } else { 41 | return false 42 | } 43 | } 44 | 45 | func imagePullCheck(imageName string) bool { 46 | // Create a new Docker client 47 | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) 48 | if err != nil { 49 | fmt.Println(err) 50 | os.Exit(1) 51 | } 52 | 53 | // Check if the image can be pulled from the repository 54 | // repository := "docker.io" 55 | // authConfig := types.AuthConfig{} 56 | _, err = cli.ImagePull(context.Background(), imageName, types.ImagePullOptions{}) 57 | if err != nil { 58 | return false 59 | // fmt.Println("Image cannot be pulled from the repository.") 60 | // fmt.Println(err) 61 | // os.Exit(1) 62 | } 63 | 64 | return true 65 | } 66 | -------------------------------------------------------------------------------- /validator/kubernetes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "k8s.io/apimachinery/pkg/api/errors" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/client-go/kubernetes" 12 | "k8s.io/client-go/tools/clientcmd" 13 | ) 14 | 15 | func checkSecret(secretName, namespace string, keys []string) ([]string, error) { 16 | findings := []string{} 17 | // Create a new Kubernetes client 18 | // Load the kubeconfig file 19 | kubeconfig := os.Getenv("KUBECONFIG") 20 | if kubeconfig == "" { 21 | // Get the user's home directory 22 | home, _ := os.UserHomeDir() 23 | 24 | // Append the default kubeconfig file path to the home directory 25 | kubeconfig = filepath.Join(home, ".kube", "config") 26 | } 27 | config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) 28 | if err != nil { 29 | return findings, nil 30 | } 31 | clientset, err := kubernetes.NewForConfig(config) 32 | if err != nil { 33 | return findings, nil 34 | } 35 | 36 | // Check if the secret exists 37 | secretRes, err := clientset.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, metav1.GetOptions{}) 38 | if errors.IsNotFound(err) { 39 | return append(findings, fmt.Sprintf("secret refered %v does not exists", secretName)), nil 40 | } else if !errors.IsNotFound(err) { 41 | // Check if the keys exist in the secret 42 | for _, key := range keys { 43 | _, ok := secretRes.Data[key] 44 | if !ok { 45 | return append(findings, fmt.Sprintf("key '%s' does not exist in secret '%s'", key, secretName)), nil 46 | } 47 | } 48 | } 49 | if err != nil { 50 | return findings, nil 51 | } 52 | 53 | return findings, nil 54 | } 55 | 56 | func checkConfigMap(configMapName, namespace string, keys []string) ([]string, error) { 57 | findings := []string{} 58 | // Create a new Kubernetes client 59 | // Load the kubeconfig file 60 | kubeconfig := os.Getenv("KUBECONFIG") 61 | if kubeconfig == "" { 62 | // Get the user's home directory 63 | home, _ := os.UserHomeDir() 64 | 65 | // Append the default kubeconfig file path to the home directory 66 | kubeconfig = filepath.Join(home, ".kube", "config") 67 | } 68 | config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) 69 | if err != nil { 70 | return findings, nil 71 | } 72 | clientset, err := kubernetes.NewForConfig(config) 73 | if err != nil { 74 | return findings, nil 75 | } 76 | 77 | // Check if the ConfigMap exists 78 | configMapData, err := clientset.CoreV1().ConfigMaps(namespace).Get(context.TODO(), configMapName, metav1.GetOptions{}) 79 | if errors.IsNotFound(err) { 80 | return append(findings, fmt.Sprintf("configmap refered %v does not exists", configMapName)), nil 81 | } else if !errors.IsNotFound(err) { 82 | // Check if the keys exist in the secret 83 | for _, key := range keys { 84 | _, ok := configMapData.Data[key] 85 | if !ok { 86 | return append(findings, fmt.Sprintf("key '%s' does not exist in configmap '%s'", key, configMapName)), nil 87 | } 88 | } 89 | } 90 | if err != nil { 91 | return findings, nil 92 | } 93 | 94 | return findings, nil 95 | } 96 | -------------------------------------------------------------------------------- /validator/pods.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | v1 "k8s.io/api/core/v1" 7 | ) 8 | 9 | func podCheck(ns string, spec *v1.PodSpec) ([]string, error) { 10 | findings := []string{} 11 | 12 | for _, c := range spec.Containers { 13 | 14 | if c.ImagePullPolicy == v1.PullNever { 15 | findings = append(findings, fmt.Sprintf("Image %v exists? %v", c.Image, imageCheck(c.Image))) 16 | } 17 | 18 | if c.ImagePullPolicy == v1.PullAlways { 19 | findings = append(findings, fmt.Sprintf("Image %v pullable? %v", c.Image, imageCheck(c.Image))) 20 | } 21 | 22 | if c.ImagePullPolicy == v1.PullIfNotPresent { 23 | findings = append(findings, fmt.Sprintf("Image %v pullable? %v", c.Image, imageCheck(c.Image))) 24 | } 25 | 26 | // Referenced secret does not exists 27 | for _, env := range c.EnvFrom { 28 | if env.SecretRef != nil { 29 | secretName := env.SecretRef.Name 30 | res, err := checkSecret(secretName, ns, []string{}) 31 | if err != nil && len(res) != 0 { 32 | return append(findings, res...), err 33 | } 34 | findings = append(findings, res...) 35 | } 36 | 37 | if env.ConfigMapRef != nil { 38 | configMapName := env.ConfigMapRef.Name 39 | res, err := checkConfigMap(configMapName, ns, []string{}) 40 | if err != nil && len(res) != 0 { 41 | return append(findings, res...), err 42 | } 43 | findings = append(findings, res...) 44 | } 45 | } 46 | 47 | for _, env := range c.Env { 48 | if env.ValueFrom.ConfigMapKeyRef != nil { 49 | configMapName := env.ValueFrom.ConfigMapKeyRef.Name 50 | configMapKey := env.ValueFrom.ConfigMapKeyRef.Key 51 | res, err := checkConfigMap(configMapName, ns, []string{configMapKey}) 52 | if err != nil && len(res) != 0 { 53 | return append(findings, res...), err 54 | } 55 | findings = append(findings, res...) 56 | } 57 | 58 | if env.ValueFrom.SecretKeyRef != nil { 59 | secretName := env.ValueFrom.SecretKeyRef.Name 60 | secretKey := env.ValueFrom.SecretKeyRef.Key 61 | res, err := checkSecret(secretName, ns, []string{secretKey}) 62 | if err != nil && len(res) != 0 { 63 | return append(findings, res...), err 64 | } 65 | findings = append(findings, res...) 66 | } 67 | 68 | } 69 | } 70 | 71 | for _, v := range spec.Volumes { 72 | if v.Secret != nil { 73 | if v.Secret.SecretName != "" { 74 | res, err := checkSecret(v.Secret.SecretName, ns, []string{}) 75 | if err != nil && len(res) != 0 { 76 | return append(findings, res...), err 77 | } 78 | findings = append(findings, res...) 79 | } 80 | } 81 | 82 | if v.ConfigMap != nil { 83 | if v.ConfigMap.Name != "" { 84 | keys := []string{} 85 | for _, ktp := range v.ConfigMap.Items { 86 | keys = append(keys, ktp.Key) 87 | } 88 | res, err := checkConfigMap(v.ConfigMap.Name, ns, []string{}) 89 | if err != nil && len(res) != 0 { 90 | return append(findings, res...), err 91 | } 92 | findings = append(findings, res...) 93 | } 94 | } 95 | } 96 | 97 | return findings, nil 98 | } 99 | -------------------------------------------------------------------------------- /validator/validation.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/fatih/color" 9 | appsv1 "k8s.io/api/apps/v1" 10 | v1 "k8s.io/api/core/v1" 11 | 12 | "k8s.io/client-go/kubernetes/scheme" 13 | ) 14 | 15 | func valiations() { 16 | // consolescanner := bufio.NewScanner(os.Stdin) 17 | 18 | // Check if any arguments were passed 19 | if len(os.Args) == 1 { 20 | fmt.Println("No arguments were passed.") 21 | return 22 | } 23 | 24 | // Print the command-line arguments 25 | // for i, arg := range os.Args[1:] { 26 | // fmt.Printf("Argument %d: %s\n", i+1, arg) 27 | // } 28 | 29 | f, err := os.ReadFile(os.Args[1]) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | // fmt.Println(consolescanner.Text()) 35 | 36 | obj, _, err := scheme.Codecs.UniversalDeserializer().Decode(f, nil, nil) 37 | if err != nil { 38 | log.Fatalf(fmt.Sprintf("Error while decoding YAML object. Err was: %s", err)) 39 | return 40 | } 41 | 42 | // now use switch over the type of the object 43 | // and match each type-case 44 | switch o := obj.(type) { 45 | case *v1.Pod: 46 | if o.Namespace == "" { 47 | o.Namespace = "default" 48 | } 49 | findngs, err := podCheck(o.Namespace, &o.Spec) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | 54 | color.Green("Findings") 55 | // Print the list of strings 56 | for i, item := range findngs { 57 | fmt.Printf("%d: %s\n", i+1, item) 58 | } 59 | 60 | case *appsv1.Deployment: 61 | podCheck(o.Namespace, &o.Spec.Template.Spec) 62 | case *appsv1.StatefulSet: 63 | podCheck(o.Namespace, &o.Spec.Template.Spec) 64 | case *appsv1.DaemonSet: 65 | podCheck(o.Namespace, &o.Spec.Template.Spec) 66 | default: 67 | fmt.Printf("Type %v is unknown", o) 68 | } 69 | } 70 | --------------------------------------------------------------------------------