├── .circleic └── config.yml ├── .gcloudignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── pull_request_template.md ├── .gitignore ├── CREDITS ├── LICENSE ├── README.md ├── cmd └── scheduler │ ├── cmd │ ├── restart.go │ ├── root.go │ └── stop.go │ └── scheduler.go ├── go.mod ├── go.sum ├── model └── model.go ├── operator ├── config.go ├── gce.go ├── gke.go ├── instance_group.go └── sql.go ├── report └── notice.go ├── scheduler.go └── scheduler └── scheduler.go /.circleic/config.yml: -------------------------------------------------------------------------------- 1 | # Golang CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-go/ for more details 4 | version: 2 5 | jobs: 6 | build: 7 | docker: 8 | - image: circleci/golang:1.12 9 | environment: 10 | GO111MODULE: "ON" 11 | working_directory: /go/src/github.com/{{ORG_NAME}}/{{REPO_NAME}} 12 | steps: 13 | - checkout 14 | - run: go mod download 15 | - run: go test -v ./... 16 | - restore_cache: 17 | keys: 18 | - go-mod-v1-{{ checksum "go.sum" }} 19 | - save_cache: 20 | key: go-mod-v1-{{ checksum "go.sum" }} 21 | paths: 22 | - "/go/pkg/mod" 23 | -------------------------------------------------------------------------------- /.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | node_modules 17 | #!include:.gitignore 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | A clear and concise description of what the bug is. 12 | 13 | ## Stacktrace Copy 14 | all of the output of the command, including the stacktrace if visible. 15 | 16 | ## To Reproduce 17 | Steps to reproduce the behavior. 18 | Include the full command being run as well as, if possible, artifacts the bug can be reproduced with. 19 | 20 | ## Expected behavior 21 | A clear and concise description of what you expected to happen. 22 | 23 | ## Known workaround 24 | If you have found a workaround, please specify what it is. 25 | 26 | ## Additional context 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Is your feature request related to a problem? Please describe. 11 | A clear and concise description of what the problem is. Ex. It's very inconvenient to have to do [...] 12 | 13 | ## Describe the solution you'd like 14 | A clear and concise description of what you want to happen. 15 | 16 | ## Describe alternatives you've considered 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | ## Additional context 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Changes proposed in this pull request 2 | * For issue #xxx 3 | 4 | # What did you Implement: 5 | * xxx 6 | 7 | # How Has This Been Tested? 8 | * xxx 9 | 10 | # Reference 11 | > https://blog.github.com/2015-01-21-how-to-write-the-perfect-pull-request/ 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | *.go~ 8 | *.un~ 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | ### Goland ### 17 | .idea 18 | 19 | # Prohibit accidentally committing gcp credentials file 20 | *.json 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright (c) 2019-present Future Corporation 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gcp-instance-scheduler 2 | [![Apache 2.0 License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/future-architect/gcp-instance-scheduler)](https://goreportcard.com/report/github.com/future-architect/gcp-instance-scheduler) 4 | 5 | Tools that shutdown GCP Instance on your schedule. 6 | 7 | ## Abstract 8 | 9 | * Shutdown target 10 | * GCE, GKE, SQL 11 | * The label `state-scheduler: true` is required to stop / restart the instance. 12 | * In order to be processed, it is necessary to assign a label to Instance, InstanceGroup or Cluster. 13 | If a label is assigned to Cluster or InstanceGroup, this tool will reduce the size of InstanceGroup to 0. 14 | * Architecture 15 | * Cloud Scheduler --> Pub/Sub --> CloudFunction 16 | * https://cloud.google.com/scheduler/docs/start-and-stop-compute-engine-instances-on-a-schedule 17 | 18 | ## Config 19 | 20 | * nothing special 21 | * [GCP_PROJECT is automation set by CloudFunction](https://cloud.google.com/functions/docs/concepts/go-runtime#contextcontext) 22 | 23 | ## QuickStart 24 | 25 | Shutdown instances with CLI command. 26 | 27 | ### Install 28 | 29 | ```bash 30 | go get -u github.com/future-architect/gcp-instance-scheduler/cmd/scheduler 31 | ``` 32 | 33 | ### Usage 34 | 35 | Need to set `GOOGLE_APPLICATION_CREDENTIALS` in environment variables before cli execution. 36 | [See setup](https://cloud.google.com/docs/authentication/getting-started) 37 | 38 | And then you set label to gcp resource 39 | ```bash 40 | gcloud compute instances create --zone us-central1-a 41 | gcloud compute instances update --project --update-labels state-scheduler=true 42 | ``` 43 | Then you can do below commands. 44 | 45 | ```bash 46 | # stop 47 | $ scheduler stop --project 48 | 49 | # restart 50 | $ scheduler restart --project 51 | ``` 52 | 53 | 54 | #### Options 55 | 56 | You can designate project id and timeout length by using flags. 57 | If you use slack notification, you have to enable slack notification by adding the flag `--slackNotifyEnable`. 58 | 59 | ```console 60 | >scheduler stop --help 61 | stop is execution command that shutdown gcp resources that assigned target label. 62 | 63 | Usage: 64 | scheduler stop [flags] 65 | 66 | Flags: 67 | -h, --help help for stop 68 | -p, --project string project id (default $GCP_PROJECT) 69 | -c, --slackChannel string Slack Channel name (should enable slack notify) (default SLACK_CHANNEL) 70 | -s, --slackNotifyEnable Enable slack notification 71 | -t, --slackToken string SlackAPI token (should enable slack notify) (default $SLACK_API_TOKEN) 72 | --timeout int set timeout seconds (default 60) 73 | 74 | 75 | >scheduler restart --help 76 | restart is launch shutdown gcp resource. 77 | 78 | Usage: 79 | scheduler restart [flags] 80 | 81 | Flags: 82 | -h, --help help for restart 83 | -p, --project string project id (default $GCP_PROJECT) 84 | -c, --slackChannel string Slack Channel name (should enable slack notify) (default SLACK_CHANNEL) 85 | -s, --slackNotifyEnable Enable slack notification 86 | -t, --slackToken string SlackAPI token (should enable slack notify) (default $SLACK_API_TOKEN) 87 | --timeout int set timeout seconds (default 60) 88 | ``` 89 | 90 | Following variables are used when you did not designate these flags. 91 | 92 | |# |flags |variables | 93 | |---|-----------------------|----------------| 94 | | 1 |project(p) |GCP_PROJECT | 95 | | 2 |slackToken |SLACK_API_TOKEN | 96 | | 3 |slackChannel |SLACK_CHANNEL | 97 | 98 | 99 | ## Example: create target resources 100 | 101 | Set label for target instance 102 | 103 | ```sh 104 | # GCE 105 | gcloud compute instances update \ 106 | --project \ 107 | --update-labels state-scheduler=true 108 | 109 | # Instance Group 110 | gcloud compute instance-templates create ... \ 111 | --project \ 112 | --labels state-scheduler=true 113 | 114 | # Cloud SQL (master must be running) 115 | gcloud beta sql instances patch \ 116 | --project \ 117 | --update-labels state-scheduler=true 118 | 119 | # GKE 120 | gcloud container clusters update \ 121 | --project \ 122 | --zone \ 123 | --update-labels state-scheduler=true,restore-size-= 124 | ``` 125 | 126 | 127 | ## Deploy to GCP CloudFunction 128 | 129 | * install [gcloud](https://cloud.google.com/sdk/gcloud/) 130 | 131 | ### Required variables 132 | When you want to get slack notification, please set these environment variables. 133 | You can get slack notification if and only if these three variables are set. 134 | 135 | |# |variables |Note | 136 | |---|----------------|-----------------------------------| 137 | | 1 |SLACK_ENABLE |Slack notification enable ("true") | 138 | | 2 |SLACK_API_TOKEN |Slack api token | 139 | | 3 |SLACK_CHANNEL |Slack channel name | 140 | 141 | ### Steps 142 | 143 | As an example, start an instance between 9 and 22:00 on weekdays. 144 | 145 | ```sh 146 | # Deploy Cloud Function: slack notification enable 147 | gcloud functions deploy switchInstanceState --project \ 148 | --entry-point SwitchInstanceState --runtime go111 \ 149 | --trigger-topic instance-scheduler-event \ 150 | --set-env-vars SLACK_ENABLE=false 151 | 152 | # Create Cloud Scheduler Job(Stop) 153 | gcloud beta scheduler jobs create pubsub shutdown-workday \ 154 | --project \ 155 | --schedule '0 22 * * 1-5' \ 156 | --topic instance-scheduler-event \ 157 | --message-body '{"command":"stop"}' \ 158 | --time-zone 'Asia/Tokyo' \ 159 | --description 'automatically stop instances' 160 | 161 | # Create Cloud Scheduler Job(Start) 162 | gcloud beta scheduler jobs create pubsub restart-workday \ 163 | --project \ 164 | --schedule '0 9 * * 1-5' \ 165 | --topic instance-scheduler-event \ 166 | --message-body '{"command":"start"}' \ 167 | --time-zone 'Asia/Tokyo' \ 168 | --description 'automatically restart instances' 169 | ``` 170 | 171 | 172 | ## Tips: Debug Function 173 | 174 | * publish message to pub/sub 175 | * `gcloud pubsub topics publish stop-instance-event --project --message "{"command":"stop"}"` 176 | * confirm Functions log 177 | * `gcloud functions logs read --project --limit 50` 178 | * manual launch for job of scheduler 179 | * `gcloud beta scheduler jobs run shutdown-workday-instance` 180 | 181 | ## License 182 | 183 | This project is licensed under the Apache License 2.0 License - see the [LICENSE](LICENSE) file for details 184 | -------------------------------------------------------------------------------- /cmd/scheduler/cmd/restart.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/future-architect/gcp-instance-scheduler/scheduler" 7 | "github.com/spf13/cobra" 8 | "log" 9 | "os" 10 | "time" 11 | ) 12 | 13 | var restartCmd = &cobra.Command{ 14 | Use: "restart", 15 | Short: "restart is launch shutdown gcp resource", 16 | Long: `restart is launch shutdown gcp resource.`, 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | projectID, slackToken, slackChannel, timeout, slackEnable, err := getFlags(cmd) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | log.Printf("Project ID: %v", projectID) 24 | if projectID == "" { 25 | return errors.New("not found project variable") 26 | } 27 | 28 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) 29 | defer cancel() 30 | 31 | return scheduler.Restart(ctx, scheduler.NewOptions(projectID, slackToken, slackChannel, slackEnable)) 32 | }, 33 | } 34 | 35 | func init() { 36 | restartCmd.PersistentFlags().StringP("project", "p", os.Getenv("GCP_PROJECT"), "project id (default $GCP_PROJECT)") 37 | restartCmd.PersistentFlags().StringP("slackToken", "t", os.Getenv("SLACK_API_TOKEN"), "SlackAPI token (should enable slack notify) (default $SLACK_API_TOKEN)") 38 | restartCmd.PersistentFlags().StringP("slackChannel", "c", os.Getenv("SLACK_CHANNEL"), "Slack Channel name (should enable slack notify) (default SLACK_CHANNEL)") 39 | restartCmd.PersistentFlags().BoolP("slackNotifyEnable", "s", false, "Enable slack notification") 40 | restartCmd.PersistentFlags().Int("timeout", 60, "set timeout seconds") 41 | 42 | rootCmd.AddCommand(restartCmd) 43 | } 44 | -------------------------------------------------------------------------------- /cmd/scheduler/cmd/root.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2019-present Future Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package cmd 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | // rootCmd represents the base command when called without any sub commands 26 | var rootCmd = &cobra.Command{ 27 | Use: "scheduler", 28 | Short: "gcp-instance-scheduler local execution entry point", 29 | } 30 | 31 | // Execute adds all child commands to the root command and sets flags appropriately. 32 | // This is called by main.main(). It only needs to happen once to the rootCmd. 33 | func Execute() { 34 | if err := rootCmd.Execute(); err != nil { 35 | fmt.Println(err) 36 | os.Exit(1) 37 | } 38 | } 39 | 40 | func getFlags(c *cobra.Command) (project, slackToken, slackChannel string, timeout int, slackEnable bool, err error) { 41 | if project, err = c.PersistentFlags().GetString("project"); err != nil { 42 | return 43 | } 44 | if timeout, err = c.PersistentFlags().GetInt("timeout"); err != nil { 45 | return 46 | } 47 | if slackToken, err = c.PersistentFlags().GetString("slackToken"); err != nil { 48 | return 49 | } 50 | if slackChannel, err = c.PersistentFlags().GetString("slackChannel"); err != nil { 51 | return 52 | } 53 | if slackEnable, err = c.PersistentFlags().GetBool("slackNotifyEnable"); err != nil { 54 | return 55 | } 56 | return 57 | } 58 | -------------------------------------------------------------------------------- /cmd/scheduler/cmd/stop.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/future-architect/gcp-instance-scheduler/scheduler" 7 | "github.com/spf13/cobra" 8 | "log" 9 | "os" 10 | "time" 11 | ) 12 | 13 | var stopCmd = &cobra.Command{ 14 | Use: "stop", 15 | Short: "stop is execution command that shutdown all gcp resources that assigned target label", 16 | Long: `stop is execution command that shutdown gcp resources that assigned target label.`, 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | projectID, slackToken, slackChannel, timeout, slackEnable, err := getFlags(cmd) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | log.Printf("Project ID: %v", projectID) 24 | if projectID == "" { 25 | return errors.New("not found project variable") 26 | } 27 | 28 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) 29 | defer cancel() 30 | 31 | return scheduler.Shutdown(ctx, scheduler.NewOptions(projectID, slackToken, slackChannel, slackEnable)) 32 | }, 33 | } 34 | 35 | func init() { 36 | stopCmd.PersistentFlags().StringP("project", "p", os.Getenv("GCP_PROJECT"), "project id (default $GCP_PROJECT)") 37 | stopCmd.PersistentFlags().StringP("slackToken", "t", os.Getenv("SLACK_API_TOKEN"), "SlackAPI token (should enable slack notify) (default $SLACK_API_TOKEN)") 38 | stopCmd.PersistentFlags().StringP("slackChannel", "c", os.Getenv("SLACK_CHANNEL"), "Slack Channel name (should enable slack notify) (default SLACK_CHANNEL)") 39 | stopCmd.PersistentFlags().BoolP("slackNotifyEnable", "s", false, "Enable slack notification") 40 | stopCmd.PersistentFlags().Int("timeout", 60, "set timeout seconds") 41 | 42 | rootCmd.AddCommand(stopCmd) 43 | } 44 | -------------------------------------------------------------------------------- /cmd/scheduler/scheduler.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2019-present Future Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package main 17 | 18 | import "github.com/future-architect/gcp-instance-scheduler/cmd/scheduler/cmd" 19 | 20 | func main() { 21 | cmd.Execute() 22 | } 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/future-architect/gcp-instance-scheduler 2 | 3 | go 1.12 4 | 5 | require ( 6 | cloud.google.com/go v0.40.0 7 | github.com/deckarep/golang-set v1.7.1 8 | github.com/hashicorp/go-multierror v1.0.0 9 | github.com/kelseyhightower/envconfig v1.4.0 10 | github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 // indirect 11 | github.com/lusis/slack-test v0.0.0-20190426140909-c40012f20018 // indirect 12 | github.com/mitchellh/go-homedir v1.1.0 13 | github.com/nlopes/slack v0.5.0 14 | github.com/pkg/errors v0.8.1 // indirect 15 | github.com/spf13/cobra v0.0.5 16 | github.com/spf13/viper v1.4.0 17 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 18 | google.golang.org/api v0.6.0 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= 4 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 5 | cloud.google.com/go v0.40.0 h1:FjSY7bOj+WzJe6TZRVtXI2b9kAYvtNg4lMbcH2+MUkk= 6 | cloud.google.com/go v0.40.0/go.mod h1:Tk58MuI9rbLMKlAjeO/bDnteAx7tX2gJIXw4T5Jwlro= 7 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 8 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 9 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 10 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 11 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 12 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 13 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 14 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 15 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 16 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 17 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 18 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 19 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 20 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 21 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 22 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 23 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 24 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ= 27 | github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= 28 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 29 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 30 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 31 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 32 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 33 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 34 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 35 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 36 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 37 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 38 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 39 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 40 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 41 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 42 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 43 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 44 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 45 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 46 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 47 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 48 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 49 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 50 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 51 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 52 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 53 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 54 | github.com/googleapis/gax-go/v2 v2.0.4 h1:hU4mGcQI4DaAYW+IbTun+2qEZVFxK0ySjQLTbS0VQKc= 55 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 56 | github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= 57 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 58 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 59 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 60 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 61 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 62 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 63 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 64 | github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= 65 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 66 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 67 | github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= 68 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 69 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 70 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 71 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 72 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 73 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 74 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 75 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 76 | github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= 77 | github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= 78 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 79 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 80 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 81 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 82 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 83 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 84 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 85 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 86 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 87 | github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 h1:AsEBgzv3DhuYHI/GiQh2HxvTP71HCCE9E/tzGUzGdtU= 88 | github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5/go.mod h1:c2mYKRyMb1BPkO5St0c/ps62L4S0W2NAkaTXj9qEI+0= 89 | github.com/lusis/slack-test v0.0.0-20190426140909-c40012f20018 h1:MNApn+Z+fIT4NPZopPfCc1obT6aY3SVM6DOctz1A9ZU= 90 | github.com/lusis/slack-test v0.0.0-20190426140909-c40012f20018/go.mod h1:sFlOUpQL1YcjhFVXhg1CG8ZASEs/Mf1oVb6H75JL/zg= 91 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= 92 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 93 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 94 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 95 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 96 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 97 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 98 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 99 | github.com/nlopes/slack v0.5.0 h1:NbIae8Kd0NpqaEI3iUrsuS0KbcEDhzhc939jLW5fNm0= 100 | github.com/nlopes/slack v0.5.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM= 101 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 102 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 103 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 104 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 105 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 106 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 107 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 108 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 109 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 110 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 111 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 112 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 113 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 114 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 115 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 116 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 117 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 118 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 119 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 120 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 121 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 122 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 123 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 124 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 125 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 126 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 127 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= 128 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 129 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 130 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 131 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 132 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 133 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 134 | github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= 135 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 136 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 137 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 138 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 139 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 140 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 141 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 142 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 143 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 144 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 145 | go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= 146 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 147 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 148 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 149 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 150 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 151 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 152 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 153 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 154 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 155 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 156 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 157 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 158 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 159 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 160 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 161 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 162 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 163 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 164 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 165 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 166 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 167 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 168 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= 169 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 170 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 171 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 172 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= 173 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 174 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 175 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 176 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 177 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 178 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 179 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 180 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 181 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 182 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 183 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 184 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 185 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 186 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA= 187 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 188 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 189 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 190 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 191 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 192 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 193 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 194 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 195 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 196 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 197 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 198 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 199 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 200 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 201 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 202 | google.golang.org/api v0.6.0 h1:2tJEkRfnZL5g1GeBUlITh/rqT5HG3sFcoVCUUxmgJ2g= 203 | google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4= 204 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 205 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 206 | google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= 207 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 208 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 209 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 210 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 211 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg= 212 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 213 | google.golang.org/genproto v0.0.0-20190530194941-fb225487d101 h1:wuGevabY6r+ivPNagjUXGGxF+GqgMd+dBhjsxW4q9u4= 214 | google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= 215 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 216 | google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= 217 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 218 | google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0= 219 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 220 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 221 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 222 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 223 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 224 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 225 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 226 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 227 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 228 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 229 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 230 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 231 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 232 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 233 | -------------------------------------------------------------------------------- /model/model.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2019-present Future Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package model 17 | 18 | import ( 19 | "fmt" 20 | ) 21 | 22 | const ( 23 | ComputeEngine = "ComputeEngine" 24 | InstanceGroup = "InstanceGroup" 25 | GKENodePool = "GKENodePool" 26 | SQL = "SQL" 27 | ) 28 | 29 | type Report struct { 30 | // InstanceGroup, ComputeEngine, SQL 31 | InstanceType string 32 | // shutdown resource names 33 | Dones []string 34 | // already stopped resource names 35 | Alreadies []string 36 | // skipped resource name 37 | Skips []string 38 | } 39 | 40 | func (r *Report) Show() []string { 41 | var lines []string 42 | lines = append(lines, "."+r.InstanceType) 43 | 44 | lines = append(lines, fmt.Sprintf(" └- Done: %v", len(r.Dones))) 45 | for _, resource := range r.Dones { 46 | lines = append(lines, fmt.Sprintf(" └-- %v", resource)) 47 | } 48 | 49 | lines = append(lines, fmt.Sprintf(" └- AlreadyDone: %v", len(r.Alreadies))) 50 | for _, resource := range r.Alreadies { 51 | lines = append(lines, fmt.Sprintf(" └-- %v", resource)) 52 | } 53 | 54 | lines = append(lines, fmt.Sprintf(" └- Skip: %v", len(r.Skips))) 55 | for _, resource := range r.Skips { 56 | lines = append(lines, fmt.Sprintf(" └-- %v", resource)) 57 | } 58 | 59 | return lines 60 | } 61 | -------------------------------------------------------------------------------- /operator/config.go: -------------------------------------------------------------------------------- 1 | package operator 2 | 3 | import "time" 4 | 5 | // API call interval 6 | const CallInterval = 50 * time.Millisecond 7 | -------------------------------------------------------------------------------- /operator/gce.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2019-present Future Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package operator 17 | 18 | import ( 19 | "errors" 20 | "strings" 21 | "time" 22 | 23 | "github.com/future-architect/gcp-instance-scheduler/model" 24 | "github.com/hashicorp/go-multierror" 25 | "golang.org/x/net/context" 26 | "google.golang.org/api/compute/v1" 27 | ) 28 | 29 | type ComputeEngineCall struct { 30 | s *compute.Service 31 | call *compute.InstancesAggregatedListCall 32 | projectID string 33 | error error 34 | } 35 | 36 | func ComputeEngine(ctx context.Context, projectID string) *ComputeEngineCall { 37 | s, err := compute.NewService(ctx) 38 | if err != nil { 39 | return &ComputeEngineCall{error: err} 40 | } 41 | 42 | // get all instances in each zone at this project 43 | return &ComputeEngineCall{ 44 | s: s, 45 | projectID: projectID, 46 | call: compute.NewInstancesService(s).AggregatedList(projectID), 47 | } 48 | } 49 | 50 | func (r *ComputeEngineCall) Filter(labelName, value string) *ComputeEngineCall { 51 | if r.error != nil { 52 | return r 53 | } 54 | r.call = r.call.Filter("labels." + labelName + "=" + value) 55 | return r 56 | } 57 | 58 | func (r *ComputeEngineCall) Stop() (*model.Report, error) { 59 | if r.error != nil { 60 | return nil, r.error 61 | } 62 | 63 | list, err := r.call.Do() 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | var res = r.error 69 | var doneRes []string 70 | var alreadyRes []string 71 | 72 | for _, instance := range valuesGCE(list.Items) { 73 | // check a instance which was already stopped 74 | if instance.Status == "STOPPED" || instance.Status == "STOPPING" || instance.Status == "TERMINATED" || 75 | instance.Status == "PROVISIONING" || instance.Status == "REPAIRING" { 76 | alreadyRes = append(alreadyRes, instance.Name) 77 | continue 78 | } 79 | 80 | // get zone name 81 | urlElements := strings.Split(instance.Zone, "/") 82 | zone := urlElements[len(urlElements)-1] 83 | 84 | _, err = compute.NewInstancesService(r.s).Stop(r.projectID, zone, instance.Name).Do() 85 | if err != nil { 86 | res = multierror.Append(res, errors.New(instance.Name+" stopping failed: %v"+err.Error())) 87 | } 88 | 89 | doneRes = append(doneRes, instance.Name) 90 | time.Sleep(CallInterval) 91 | } 92 | 93 | return &model.Report{ 94 | InstanceType: model.ComputeEngine, 95 | Dones: doneRes, 96 | Alreadies: alreadyRes, 97 | }, res 98 | } 99 | 100 | func (r *ComputeEngineCall) Start() (*model.Report, error) { 101 | if r.error != nil { 102 | return nil, r.error 103 | } 104 | 105 | list, err := r.call.Do() 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | var res = r.error 111 | var doneRes []string 112 | var alreadyRes []string 113 | 114 | for _, instance := range valuesGCE(list.Items) { 115 | // check a instance which was already running 116 | if instance.Status == "RUNNING" || instance.Status == "PROVISIONING" || instance.Status == "REPAIRING" { 117 | alreadyRes = append(alreadyRes, instance.Name) 118 | continue 119 | } 120 | 121 | // get zone name 122 | urlElements := strings.Split(instance.Zone, "/") 123 | zone := urlElements[len(urlElements)-1] 124 | 125 | _, err = compute.NewInstancesService(r.s).Start(r.projectID, zone, instance.Name).Do() 126 | if err != nil { 127 | res = multierror.Append(res, err) 128 | } 129 | 130 | doneRes = append(doneRes, instance.Name) 131 | time.Sleep(CallInterval) 132 | } 133 | 134 | return &model.Report{ 135 | InstanceType: model.ComputeEngine, 136 | Dones: doneRes, 137 | Alreadies: alreadyRes, 138 | }, res 139 | } 140 | 141 | // create instance list 142 | func valuesGCE(m map[string]compute.InstancesScopedList) []*compute.Instance { 143 | var res []*compute.Instance 144 | for _, instanceList := range m { 145 | if len(instanceList.Instances) == 0 { 146 | continue 147 | } 148 | res = append(res, instanceList.Instances...) 149 | } 150 | return res 151 | } 152 | -------------------------------------------------------------------------------- /operator/gke.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2019-present Future Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package operator 17 | 18 | import ( 19 | "errors" 20 | "fmt" 21 | set "github.com/deckarep/golang-set" 22 | "github.com/future-architect/gcp-instance-scheduler/model" 23 | "github.com/hashicorp/go-multierror" 24 | "golang.org/x/net/context" 25 | "google.golang.org/api/compute/v1" 26 | "google.golang.org/api/container/v1" 27 | "strconv" 28 | "strings" 29 | "time" 30 | "regexp" 31 | ) 32 | 33 | type GKENodePoolCall struct { 34 | targetLabel string 35 | projectID string 36 | error error 37 | s *compute.Service 38 | ctx context.Context 39 | targetLabelValue string 40 | } 41 | 42 | func GKENodePool(ctx context.Context, projectID string) *GKENodePoolCall { 43 | s, err := compute.NewService(ctx) 44 | if err != nil { 45 | return &GKENodePoolCall{error: err} 46 | } 47 | 48 | // get all templates list 49 | return &GKENodePoolCall{ 50 | s: s, 51 | projectID: projectID, 52 | ctx: ctx, 53 | } 54 | } 55 | 56 | func (r *GKENodePoolCall) Filter(labelName, value string) *GKENodePoolCall { 57 | if r.error != nil { 58 | return r 59 | } 60 | r.targetLabel = labelName 61 | r.targetLabelValue = value 62 | return r 63 | } 64 | 65 | func (r *GKENodePoolCall) Resize(size int64) (*model.Report, error) { 66 | if r.error != nil { 67 | return nil, r.error 68 | } 69 | 70 | // get all instance group mangers list 71 | managerList, err := compute.NewInstanceGroupManagersService(r.s).AggregatedList(r.projectID).Do() 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | // add instance group name of cluster node pool to Set 77 | gkeNodePoolInstanceGroupSet, err := r.getGKEInstanceGroup() 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | fmt.Println("gkeNodePoolInstanceGroupSet:", gkeNodePoolInstanceGroupSet.ToSlice()) 83 | 84 | var res = r.error 85 | var alreadyRes []string 86 | var doneRes []string 87 | 88 | for _, manager := range valuesIG(managerList.Items) { 89 | 90 | fmt.Println("manager.InstanceTemplate:", manager.InstanceTemplate) 91 | fmt.Println("manager.Name:", manager.Name) 92 | 93 | // Check GKE NodePool InstanceGroup 94 | if gkeNodePoolInstanceGroupSet.Contains(manager.Name) { 95 | if !manager.Status.IsStable { 96 | continue 97 | } 98 | 99 | if manager.TargetSize == size { 100 | alreadyRes = append(alreadyRes, manager.Name) 101 | continue 102 | } 103 | 104 | // get manager zone name 105 | zoneUrlElements := strings.Split(manager.Zone, "/") 106 | zone := zoneUrlElements[len(zoneUrlElements)-1] 107 | 108 | ms := compute.NewInstanceGroupManagersService(r.s) 109 | if _, err := ms.Resize(r.projectID, zone, manager.Name, size).Do(); err != nil { 110 | res = multierror.Append(res, err) 111 | continue 112 | } 113 | doneRes = append(doneRes, manager.Name) 114 | } 115 | 116 | time.Sleep(CallInterval) 117 | } 118 | 119 | return &model.Report{ 120 | InstanceType: model.GKENodePool, 121 | Dones: doneRes, 122 | Alreadies: alreadyRes, 123 | }, res 124 | } 125 | 126 | func (r *GKENodePoolCall) Recovery() (*model.Report, error) { 127 | if r.error != nil { 128 | return nil, r.error 129 | } 130 | 131 | managerList, err := compute.NewInstanceGroupManagersService(r.s).AggregatedList(r.projectID).Do() 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | // add instance group name of cluster node pool to Set 137 | gkeNodePoolInstanceGroupSet, err := r.getGKEInstanceGroup() 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | sizeMap, err := GetOriginalNodePoolSize(r.ctx, r.projectID, r.targetLabel, r.targetLabelValue) 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | var res = r.error 148 | var doneRes []string 149 | var alreadyRes []string 150 | 151 | for _, manager := range valuesIG(managerList.Items) { 152 | 153 | // check instance group of gke node pool 154 | if gkeNodePoolInstanceGroupSet.Contains(manager.Name) { 155 | if !manager.Status.IsStable { 156 | continue 157 | } 158 | 159 | split := strings.Split(manager.InstanceGroup, "/") 160 | instanceGroupName := split[len(split)-1] 161 | 162 | originalSize := sizeMap[instanceGroupName] 163 | 164 | if manager.TargetSize == originalSize { 165 | alreadyRes = append(alreadyRes, manager.Name) 166 | continue 167 | } 168 | 169 | // get manager zone name 170 | zoneUrlElements := strings.Split(manager.Zone, "/") 171 | zone := zoneUrlElements[len(zoneUrlElements)-1] // ex) us-central1-a 172 | 173 | ms := compute.NewInstanceGroupManagersService(r.s) 174 | if _, err := ms.Resize(r.projectID, zone, manager.Name, originalSize).Do(); err != nil { 175 | res = multierror.Append(res, err) 176 | continue 177 | } 178 | doneRes = append(doneRes, manager.Name) 179 | } 180 | 181 | time.Sleep(CallInterval) 182 | } 183 | 184 | return &model.Report{ 185 | InstanceType: model.GKENodePool, 186 | Dones: doneRes, 187 | Alreadies: alreadyRes, 188 | }, res 189 | } 190 | 191 | // get target GKE instance group Set 192 | func (r *GKENodePoolCall) getGKEInstanceGroup() (set.Set, error) { 193 | s, err := container.NewService(r.ctx) 194 | if err != nil { 195 | return nil, err 196 | } 197 | 198 | // get all clusters list 199 | clusters, err := container.NewProjectsLocationsClustersService(s).List("projects/" + r.projectID + "/locations/-").Do() 200 | if err != nil { 201 | return nil, err 202 | } 203 | 204 | res := set.NewSet() 205 | for _, cluster := range filter(clusters.Clusters, r.targetLabel, r.targetLabelValue) { 206 | for _, nodePool := range cluster.NodePools { 207 | for _, gkeInstanceGroup := range nodePool.InstanceGroupUrls { 208 | tmpUrlElements := strings.Split(gkeInstanceGroup, "/") 209 | managerTemplate := tmpUrlElements[len(tmpUrlElements)-1] 210 | res.Add(managerTemplate) // e.g. gke-tky-cluster-default-pool-cb765a7d-grp 211 | } 212 | } 213 | } 214 | return res, nil 215 | } 216 | 217 | func SetLableIfNoLabel(ctx context.Context, projectID, targetLabel string) error { 218 | s, err := container.NewService(ctx) 219 | if err != nil { 220 | return err 221 | } 222 | currentNodeSize, err := GetCurrentNodeCount(ctx, projectID, targetLabel) 223 | if err != nil { 224 | return err 225 | } 226 | // get all clusters list 227 | clusters, err := container.NewProjectsLocationsClustersService(s).List("projects/" + projectID + "/locations/-").Do() 228 | if err != nil { 229 | return err 230 | } 231 | for _, cluster := range filter(clusters.Clusters, targetLabel, "true") { 232 | labels := cluster.ResourceLabels 233 | for _, nodePool := range cluster.NodePools { 234 | nodeSizeLabel := "restore-size-"+nodePool.Name 235 | _, ok := labels[nodeSizeLabel] 236 | if !ok { 237 | // set new label 238 | labels[nodeSizeLabel] = strconv.FormatInt(currentNodeSize[nodePool.Name], 10) 239 | } 240 | } 241 | parseRegion := strings.Split(cluster.Location, "/") 242 | region := parseRegion[len(parseRegion)-1] 243 | name := "projects/" + projectID + "/locations/" + region + "/clusters/" + cluster.Name 244 | req := &container.SetLabelsRequest{ 245 | ResourceLabels: labels, 246 | } 247 | // update labels 248 | _, err := container.NewProjectsLocationsClustersService(s).SetResourceLabels(name, req).Do() 249 | if err != nil { 250 | return err 251 | } 252 | } 253 | return nil 254 | } 255 | 256 | 257 | // GetOriginalNodePoolSize returns map that key=instanceGroupName and value=originalSize 258 | func GetOriginalNodePoolSize(ctx context.Context, projectID, targetLabel, labelValue string) (map[string]int64, error) { 259 | s, err := container.NewService(ctx) 260 | if err != nil { 261 | return nil, err 262 | } 263 | 264 | // get all clusters list 265 | clusters, err := container.NewProjectsLocationsClustersService(s).List("projects/" + projectID + "/locations/-").Do() 266 | if err != nil { 267 | return nil, err 268 | } 269 | 270 | result := make(map[string]int64) 271 | 272 | for _, cluster := range filter(clusters.Clusters, targetLabel, labelValue) { 273 | labels := cluster.ResourceLabels 274 | for _, nodePool := range cluster.NodePools { 275 | restoreSize, ok := labels["restore-size-"+nodePool.Name] 276 | if !ok { 277 | continue 278 | } 279 | 280 | size, err := strconv.Atoi(restoreSize) 281 | if err != nil { 282 | return nil, errors.New("label: " + "restore-size-" + nodePool.Name + " value is not number format?") 283 | } 284 | 285 | for _, url := range nodePool.InstanceGroupUrls { 286 | // u;rl is below format 287 | // e.g. https://www.googleapis.com/compute/v1/projects/{ProjectID}/zones/us-central1-a/instanceGroupManagers/gke-standard-cluster-1-default-pool-1234abcd-grp 288 | urlSplit := strings.Split(url, "/") 289 | instanceGroupName := urlSplit[len(urlSplit)-1] 290 | result[instanceGroupName] = int64(size) 291 | } 292 | } 293 | } 294 | 295 | return result, nil 296 | } 297 | 298 | // GetCurrentNodeCount returns map that key=NodePoolName and value=currentSize 299 | func GetCurrentNodeCount(ctx context.Context, projectID, targetLabel string) (map[string]int64, error) { 300 | s, err := container.NewService(ctx) 301 | if err != nil { 302 | return nil, err 303 | } 304 | computeService, err := compute.NewService(ctx) 305 | if err != nil { 306 | return nil, err 307 | } 308 | // get all clusters list 309 | clusters, err := container.NewProjectsLocationsClustersService(s).List("projects/" + projectID + "/locations/-").Do() 310 | if err != nil { 311 | return nil, err 312 | } 313 | 314 | result := make(map[string]int64) 315 | 316 | reZone := regexp.MustCompile(".*/zones/") 317 | reInstance := regexp.MustCompile(".*/instanceGroupManagers/") 318 | reEtc := regexp.MustCompile("/.*") 319 | 320 | for _, cluster := range filter(clusters.Clusters, targetLabel, "true") { 321 | for _, nodePool := range cluster.NodePools { 322 | // nodePool.InstanceGroupUrls's format is below 323 | // ["https://www.googleapis.com/compute/v1/projects//zones//instanceGroupManagers/gke-test-scheduler-2-default-pool-2b19b588-grp", 324 | // "https://www.googleapis.com/compute/v1/projects//zones//instanceGroupManagers/gke-test-scheduler-2-default-pool-2b19b588-grp", 325 | // "https://www.googleapis.com/compute/v1/projects//zones//instanceGroupManagers/gke-test-scheduler-2-default-pool-2b19b588-grp"] 326 | 327 | zone := reZone.ReplaceAllString(nodePool.InstanceGroupUrls[0], "")//"/instanceGroupManagers/gke-test-scheduler-2-default-pool-2b19b588-grp" 328 | zone = reEtc.ReplaceAllString(zone, "")//" 329 | instanceGroup := reInstance.ReplaceAllString(nodePool.InstanceGroupUrls[0], "") 330 | resp, err := computeService.InstanceGroups.Get(projectID, zone, instanceGroup).Context(ctx).Do() 331 | if err != nil { 332 | return nil, err 333 | } 334 | size := resp.Size 335 | result[nodePool.Name] = size 336 | } 337 | } 338 | return result, nil 339 | } 340 | 341 | // grep target cluster and create target cluster list 342 | func filter(l []*container.Cluster, label, value string) []*container.Cluster { 343 | if label == "" { //TODO Temp impl 344 | return l 345 | } 346 | 347 | var res []*container.Cluster 348 | for _, cluster := range l { 349 | if cluster.ResourceLabels[label] == value { 350 | res = append(res, cluster) 351 | } 352 | } 353 | return res 354 | } 355 | -------------------------------------------------------------------------------- /operator/instance_group.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2019-present Future Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package operator 17 | 18 | import ( 19 | set "github.com/deckarep/golang-set" 20 | "github.com/future-architect/gcp-instance-scheduler/model" 21 | "github.com/hashicorp/go-multierror" 22 | "golang.org/x/net/context" 23 | "google.golang.org/api/compute/v1" 24 | "strings" 25 | "time" 26 | ) 27 | 28 | type InstanceGroupCall struct { 29 | instanceGroupList *compute.InstanceGroupManagerAggregatedList 30 | templateListCall *compute.InstanceTemplatesListCall 31 | targetLabel string 32 | targetLabelValue string 33 | projectID string 34 | error error 35 | s *compute.Service 36 | ctx context.Context 37 | } 38 | 39 | func InstanceGroup(ctx context.Context, projectID string) *InstanceGroupCall { 40 | s, err := compute.NewService(ctx) 41 | if err != nil { 42 | return &InstanceGroupCall{error: err} 43 | } 44 | 45 | // get all instance group mangers list 46 | managerList, err := compute.NewInstanceGroupManagersService(s).AggregatedList(projectID).Do() 47 | if err != nil { 48 | return &InstanceGroupCall{error: err} 49 | } 50 | 51 | // get all templates list 52 | return &InstanceGroupCall{ 53 | s: s, 54 | templateListCall: compute.NewInstanceTemplatesService(s).List(projectID), 55 | instanceGroupList: managerList, 56 | projectID: projectID, 57 | ctx: ctx, 58 | } 59 | } 60 | 61 | func (r *InstanceGroupCall) Filter(labelName, value string) *InstanceGroupCall { 62 | if r.error != nil { 63 | return r 64 | } 65 | r.targetLabel = labelName 66 | r.targetLabelValue = value 67 | r.templateListCall = r.templateListCall.Filter("properties.labels." + labelName + "=" + value) 68 | return r 69 | } 70 | 71 | func (r *InstanceGroupCall) Resize(size int64) (*model.Report, error) { 72 | if r.error != nil { 73 | return nil, r.error 74 | } 75 | 76 | templateList, err := r.templateListCall.Do() 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | var res = r.error 82 | var doneRes []string 83 | var alreadyRes []string 84 | 85 | for _, manager := range valuesIG(r.instanceGroupList.Items) { 86 | // get manager zone name 87 | zoneUrlElements := strings.Split(manager.Zone, "/") 88 | zone := zoneUrlElements[len(zoneUrlElements)-1] 89 | 90 | // get manager's template name 91 | tmpUrlElements := strings.Split(manager.InstanceTemplate, "/") 92 | managerTemplate := tmpUrlElements[len(tmpUrlElements)-1] 93 | 94 | // add instance group name to Set 95 | instanceGroupSet := set.NewSet() 96 | for _, t := range templateList.Items { 97 | instanceGroupSet.Add(t.Name) 98 | } 99 | 100 | // compare filtered instance template name and manager which is created by template 101 | if instanceGroupSet.Contains(managerTemplate) { 102 | if !manager.Status.IsStable { 103 | continue 104 | } 105 | 106 | if manager.TargetSize == 0 { 107 | alreadyRes = append(alreadyRes, manager.Name) 108 | continue 109 | } 110 | 111 | ms := compute.NewInstanceGroupManagersService(r.s) 112 | if _, err := ms.Resize(r.projectID, zone, manager.Name, size).Do(); err != nil { 113 | res = multierror.Append(res, err) 114 | continue 115 | } 116 | doneRes = append(doneRes, manager.Name) 117 | } 118 | 119 | time.Sleep(CallInterval) 120 | } 121 | 122 | return &model.Report{ 123 | InstanceType: model.InstanceGroup, 124 | Dones: doneRes, 125 | Alreadies: alreadyRes, 126 | }, res 127 | } 128 | 129 | func (r *InstanceGroupCall) Recovery() (*model.Report, error) { 130 | if r.error != nil { 131 | return nil, r.error 132 | } 133 | 134 | templateList, err := r.templateListCall.Do() 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | // add instance group name to Set 140 | // NodePool that belong to GKE which has target label 141 | targetInstanceGroupSet := set.NewSet() 142 | for _, t := range templateList.Items { 143 | targetInstanceGroupSet.Add(t.Name) 144 | } 145 | 146 | sizeMap, err := GetOriginalNodePoolSize(r.ctx, r.projectID, r.targetLabel, r.targetLabelValue) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | var res = r.error 152 | var doneRes []string 153 | var alreadyRes []string 154 | 155 | for _, manager := range valuesIG(r.instanceGroupList.Items) { 156 | // get manager zone name 157 | zoneUrlElements := strings.Split(manager.Zone, "/") 158 | zone := zoneUrlElements[len(zoneUrlElements)-1] // ex) us-central1-a 159 | 160 | // get manager's template name 161 | tmpUrlElements := strings.Split(manager.InstanceTemplate, "/") 162 | instanceTemplateName := tmpUrlElements[len(tmpUrlElements)-1] // ex) gke-standard-cluster-1-default-pool-f789c8df 163 | 164 | // compare filtered instance template name and manager which is created by template 165 | if targetInstanceGroupSet.Contains(instanceTemplateName) { 166 | if !manager.Status.IsStable { 167 | continue 168 | } 169 | 170 | split := strings.Split(manager.InstanceGroup, "/") 171 | instanceGroupName := split[len(split)-1] 172 | 173 | originalSize := sizeMap[instanceGroupName] 174 | 175 | if manager.TargetSize == originalSize { 176 | alreadyRes = append(alreadyRes, manager.Name) 177 | continue 178 | } 179 | 180 | ms := compute.NewInstanceGroupManagersService(r.s) 181 | if _, err := ms.Resize(r.projectID, zone, manager.Name, originalSize).Do(); err != nil { 182 | res = multierror.Append(res, err) 183 | continue 184 | } 185 | doneRes = append(doneRes, manager.Name) 186 | } 187 | 188 | time.Sleep(CallInterval) 189 | } 190 | 191 | return &model.Report{ 192 | InstanceType: model.InstanceGroup, 193 | Dones: doneRes, 194 | Alreadies: alreadyRes, 195 | }, res 196 | } 197 | 198 | // create instance group manager list 199 | func valuesIG(m map[string]compute.InstanceGroupManagersScopedList) []*compute.InstanceGroupManager { 200 | var res []*compute.InstanceGroupManager 201 | for _, managerList := range m { 202 | if len(managerList.InstanceGroupManagers) == 0 { 203 | continue 204 | } 205 | res = append(res, managerList.InstanceGroupManagers...) 206 | } 207 | return res 208 | } 209 | -------------------------------------------------------------------------------- /operator/sql.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2019-present Future Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package operator 17 | 18 | import ( 19 | "time" 20 | 21 | "github.com/future-architect/gcp-instance-scheduler/model" 22 | 23 | "github.com/hashicorp/go-multierror" 24 | "golang.org/x/net/context" 25 | sqladmin "google.golang.org/api/sqladmin/v1beta4" 26 | ) 27 | 28 | type SQLCall struct { 29 | s *sqladmin.Service 30 | call *sqladmin.InstancesListCall 31 | projectID string 32 | error error 33 | } 34 | 35 | func SQL(ctx context.Context, projectID string) *SQLCall { 36 | s, err := sqladmin.NewService(ctx) 37 | if err != nil { 38 | return &SQLCall{error: err} 39 | } 40 | 41 | return &SQLCall{ 42 | s: s, 43 | projectID: projectID, 44 | call: sqladmin.NewInstancesService(s).List(projectID), 45 | } 46 | } 47 | 48 | func (r *SQLCall) Filter(labelName, value string) *SQLCall { 49 | if r.error != nil { 50 | return r 51 | } 52 | 53 | // Document is below format but must add "settings". GCP bugs?? 54 | // https://cloud.google.com/sql/docs/mysql/label-instance#filtering > CURL 55 | // curl --header "Authorization: Bearer ${ACCESS_TOKEN}" \ 56 | // -X GET \ 57 | // https://www.googleapis.com/sql/v1beta4/projects/[PROJECT_ID]/instances/list?filter=userLabels.[KEY1_NAME]:[KEY1_VALUE]%20userLabels.[KEY2_NAME]:[KEY2_VALUE] 58 | r.call = r.call.Filter("settings.userLabels." + labelName + "=" + value) 59 | return r 60 | } 61 | 62 | func (r *SQLCall) Stop() (*model.Report, error) { 63 | if r.error != nil { 64 | return nil, r.error 65 | } 66 | 67 | targets, err := r.call.Do() 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | var res = r.error 73 | var doneRes []string 74 | var alreadyRes []string 75 | 76 | for _, instance := range targets.Items { 77 | // do not change replica instance's activation policy 78 | if instance.InstanceType == "READ_REPLICA_INSTANCE" { 79 | continue 80 | } 81 | 82 | // do not change instance's activation policy which is already "NEVER" 83 | if instance.Settings.ActivationPolicy == "NEVER" { 84 | alreadyRes = append(alreadyRes, instance.Name) 85 | continue 86 | } 87 | 88 | // update policy 89 | instance.Settings.ActivationPolicy = "NEVER" 90 | 91 | // apply the settings 92 | _, err := sqladmin.NewInstancesService(r.s).Patch(r.projectID, instance.Name, instance).Do() 93 | if err != nil { 94 | res = multierror.Append(res, err) 95 | } 96 | doneRes = append(doneRes, instance.Name) 97 | time.Sleep(CallInterval) 98 | } 99 | 100 | return &model.Report{ 101 | InstanceType: model.SQL, 102 | Dones: doneRes, 103 | Alreadies: alreadyRes, 104 | }, res 105 | } 106 | 107 | func (r *SQLCall) Start() (*model.Report, error) { 108 | if r.error != nil { 109 | return nil, r.error 110 | } 111 | 112 | targets, err := r.call.Do() 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | var res = r.error 118 | var doneRes []string 119 | var alreadyRes []string 120 | 121 | for _, instance := range targets.Items { 122 | // do not change replica instance's activation policy 123 | if instance.InstanceType == "READ_REPLICA_INSTANCE" { 124 | continue 125 | } 126 | 127 | // do not change instance's activation policy which is already "ALWAYS" 128 | if instance.Settings.ActivationPolicy == "ALWAYS" { 129 | alreadyRes = append(alreadyRes, instance.Name) 130 | continue 131 | } 132 | 133 | // Update policy 134 | instance.Settings.ActivationPolicy = "ALWAYS" 135 | 136 | // apply the settings 137 | _, err := sqladmin.NewInstancesService(r.s).Patch(r.projectID, instance.Name, instance).Do() 138 | if err != nil { 139 | res = multierror.Append(res, err) 140 | } 141 | doneRes = append(doneRes, instance.Name) 142 | time.Sleep(CallInterval) 143 | } 144 | 145 | return &model.Report{ 146 | InstanceType: model.SQL, 147 | Dones: doneRes, 148 | Alreadies: alreadyRes, 149 | }, res 150 | } 151 | -------------------------------------------------------------------------------- /report/notice.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "fmt" 5 | "github.com/future-architect/gcp-instance-scheduler/model" 6 | "github.com/nlopes/slack" 7 | ) 8 | 9 | type Report struct { 10 | ProjectID string 11 | Command string 12 | Reports []*model.Report 13 | } 14 | 15 | type slackNotifier struct { 16 | slackAPIToken string 17 | slackChannel string 18 | } 19 | 20 | func NewSlackNotifier(slackAPIToken, slackChannel string) *slackNotifier { 21 | return &slackNotifier{ 22 | slackAPIToken: slackAPIToken, 23 | slackChannel: slackChannel, 24 | } 25 | } 26 | 27 | func (n *slackNotifier) Post(r Report) (string, error) { 28 | text := fmt.Sprintf("Project(%s) %s Report\n", r.ProjectID, r.Command) 29 | 30 | for _, detail := range r.Reports { 31 | lines := detail.Show() 32 | for _, line := range lines { 33 | text += line + "\n" 34 | } 35 | } 36 | 37 | return n.postInline(text) 38 | } 39 | 40 | func (n *slackNotifier) postInline(text string) (string, error) { 41 | _, ts, err := slack.New(n.slackAPIToken).PostMessage( 42 | n.slackChannel, 43 | slack.MsgOptionText("```"+text+"```", false), 44 | ) 45 | return ts, err 46 | } 47 | -------------------------------------------------------------------------------- /scheduler.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2019-present Future Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package function 17 | 18 | import ( 19 | "encoding/json" 20 | "errors" 21 | "log" 22 | 23 | "cloud.google.com/go/pubsub" 24 | "github.com/future-architect/gcp-instance-scheduler/scheduler" 25 | "github.com/kelseyhightower/envconfig" 26 | "golang.org/x/net/context" 27 | ) 28 | 29 | // Env is cloud function environment variables 30 | type Env struct { 31 | ProjectID string `envconfig:"GCP_PROJECT" required:"true"` 32 | SlackNotify bool `envconfig:"SLACK_ENABLE" required:"true"` 33 | SlackToken string `envconfig:"SLACK_API_TOKEN"` 34 | SlackChannel string `envconfig:"SLACK_CHANNEL"` 35 | } 36 | 37 | func SwitchInstanceState(ctx context.Context, msg *pubsub.Message) error { 38 | 39 | var e Env 40 | if err := envconfig.Process("", &e); err != nil { 41 | log.Printf("Error at the fucntion 'DecodeMessage': %v", err) 42 | return err 43 | } 44 | if e.SlackNotify && (e.SlackToken == "" || e.SlackChannel == "") { 45 | return errors.New("missing environment variable") 46 | } 47 | 48 | payload, err := decode(msg.Data) 49 | if err != nil { 50 | log.Printf("Error at the fucntion 'DecodeMessage': %v", err) 51 | return err 52 | } 53 | log.Printf("Subscribed message(Command): %v", payload.Command) 54 | 55 | log.Printf("Project ID: %v", e.ProjectID) 56 | opts := scheduler.NewOptions(e.ProjectID, e.SlackToken, e.SlackChannel, e.SlackNotify) 57 | 58 | switch payload.Command { 59 | case "start": 60 | if err := scheduler.Restart(ctx, opts); err != nil { 61 | return err 62 | } 63 | case "stop": 64 | if err := scheduler.Shutdown(ctx, opts); err != nil { 65 | return err 66 | } 67 | default: 68 | return errors.New("unknown command type") 69 | } 70 | 71 | return nil 72 | } 73 | 74 | type Payload struct { 75 | Command string `json:"command"` 76 | } 77 | 78 | func decode(payload []byte) (p Payload, err error) { 79 | if err = json.Unmarshal(payload, &p); err != nil { 80 | log.Printf("Message[%v] ... Could not decode subscribing data: %v", payload, err) 81 | if e, ok := err.(*json.SyntaxError); ok { 82 | log.Printf("syntax error at byte offset %d", e.Offset) 83 | } 84 | return 85 | } 86 | return 87 | } 88 | -------------------------------------------------------------------------------- /scheduler/scheduler.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2019-present Future Corporation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scheduler 17 | 18 | import ( 19 | "log" 20 | "strings" 21 | 22 | "github.com/future-architect/gcp-instance-scheduler/model" 23 | "github.com/future-architect/gcp-instance-scheduler/operator" 24 | "github.com/future-architect/gcp-instance-scheduler/report" 25 | 26 | "github.com/hashicorp/go-multierror" 27 | "golang.org/x/net/context" 28 | ) 29 | 30 | // Operation target label name 31 | const Label = "state-scheduler" 32 | 33 | type Options struct { 34 | Project string 35 | SlackEnable bool 36 | SlackToken string 37 | SlackChannel string 38 | } 39 | 40 | func NewOptions(projectID, slackToken, slackChannel string, slackEnable bool) *Options { 41 | return &Options{ 42 | Project: projectID, 43 | SlackEnable: slackEnable, 44 | SlackToken: slackToken, 45 | SlackChannel: slackChannel, 46 | } 47 | } 48 | 49 | func Shutdown(ctx context.Context, op *Options) error { 50 | projectID := op.Project 51 | 52 | var errorLog error 53 | var result []*model.Report 54 | 55 | 56 | if err := operator.SetLableIfNoLabel(ctx, projectID, Label); err != nil { 57 | errorLog = multierror.Append(errorLog, err) 58 | log.Printf("Error in setting labels on GKE cluster: %v", err) 59 | } 60 | rpt, err := operator.GKENodePool(ctx, projectID).Filter(Label, "true").Resize(0) 61 | if err != nil { 62 | errorLog = multierror.Append(errorLog, err) 63 | log.Printf("Some error occured in stopping gke node pool: %v", err) 64 | } else { 65 | result = append(result, rpt) 66 | log.Println(strings.Join(rpt.Show(), "\n")) 67 | } 68 | 69 | rpt, err = operator.InstanceGroup(ctx, projectID).Filter(Label, "true").Resize(0) 70 | if err != nil { 71 | errorLog = multierror.Append(errorLog, err) 72 | log.Printf("Some error occured in stopping instances group: %v", err) 73 | } else { 74 | result = append(result, rpt) 75 | log.Println(strings.Join(rpt.Show(), "\n")) 76 | } 77 | 78 | rpt, err = operator.ComputeEngine(ctx, projectID).Filter(Label, "true").Stop() 79 | if err != nil { 80 | errorLog = multierror.Append(errorLog, err) 81 | log.Printf("Some error occured in stopping gce instances: %v", err) 82 | } else { 83 | result = append(result, rpt) 84 | log.Println(strings.Join(rpt.Show(), "\n")) 85 | } 86 | 87 | rpt, err = operator.SQL(ctx, projectID).Filter(Label, "true").Stop() 88 | if err != nil { 89 | errorLog = multierror.Append(errorLog, err) 90 | log.Printf("Some error occured in stopping sql instances: %v", err) 91 | } else { 92 | result = append(result, rpt) 93 | log.Println(strings.Join(rpt.Show(), "\n")) 94 | } 95 | 96 | if !op.SlackEnable { 97 | log.Printf("done.") 98 | return errorLog 99 | } 100 | 101 | _, err = report.NewSlackNotifier(op.SlackToken, op.SlackChannel).Post(report.Report{ 102 | ProjectID: projectID, 103 | Reports: result, 104 | Command: "Shutdown", 105 | }) 106 | if err != nil { 107 | log.Println("error in Slack notification:", err) 108 | return multierror.Append(errorLog, err) 109 | } 110 | 111 | log.Printf("done.") 112 | return errorLog 113 | } 114 | 115 | func Restart(ctx context.Context, op *Options) error { 116 | projectID := op.Project 117 | 118 | var errorLog error 119 | var result []*model.Report 120 | 121 | rpt, err := operator.SQL(ctx, projectID).Filter(Label, "true").Start() 122 | if err != nil { 123 | errorLog = multierror.Append(errorLog, err) 124 | log.Printf("Some error occurred in starting SQL: %v\n", err) 125 | } else { 126 | result = append(result, rpt) 127 | log.Println(strings.Join(rpt.Show(), "\n")) 128 | } 129 | 130 | rpt, err = operator.ComputeEngine(ctx, projectID).Filter(Label, "true").Start() 131 | if err != nil { 132 | errorLog = multierror.Append(errorLog, err) 133 | log.Printf("Some error occurred in starting compute engine: %v\n", err) 134 | } else { 135 | result = append(result, rpt) 136 | log.Println(strings.Join(rpt.Show(), "\n")) 137 | } 138 | 139 | rpt, err = operator.InstanceGroup(ctx, projectID).Filter(Label, "true").Recovery() 140 | if err != nil { 141 | errorLog = multierror.Append(errorLog, err) 142 | log.Printf("Some error occurred in starting instances group: %v\n", err) 143 | } else { 144 | result = append(result, rpt) 145 | log.Println(strings.Join(rpt.Show(), "\n")) 146 | } 147 | 148 | rpt, err = operator.GKENodePool(ctx, projectID).Filter(Label, "true").Recovery() 149 | if err != nil { 150 | errorLog = multierror.Append(errorLog, err) 151 | log.Printf("Some error occurred in starting gke node pool: %v\n", err) 152 | } else { 153 | result = append(result, rpt) 154 | log.Println(strings.Join(rpt.Show(), "\n")) 155 | } 156 | 157 | if !op.SlackEnable { 158 | log.Printf("done.") 159 | return errorLog 160 | } 161 | 162 | _, err = report.NewSlackNotifier(op.SlackToken, op.SlackChannel).Post(report.Report{ 163 | ProjectID: projectID, 164 | Reports: result, 165 | Command: "Restart", 166 | }) 167 | if err != nil { 168 | log.Println("error in Slack notification:", err) 169 | return multierror.Append(errorLog, err) 170 | } 171 | 172 | log.Printf("done.") 173 | return errorLog 174 | } 175 | --------------------------------------------------------------------------------