diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..853db37 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,32 @@ +name: release +on: + workflow_dispatch: + push: + tags: + - "v*" +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4.1.7 + - name: Unshallow + run: git fetch --prune --unshallow + - name: Set up Go + uses: actions/setup-go@v5.0.2 + with: + go-version: 1.23 + - name: Import GPG key + id: import_gpg + uses: crazy-max/ghaction-import-gpg@v6.1.0 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.PASSPHRASE }} + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6.0.0 + with: + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} diff --git a/README.md b/README.md new file mode 100644 index 0000000..e90bade --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +# appe - Alerting Policy Price Estimator + +Starting January 2025, Google will begin charging for [Alerting Policies](https://cloud.google.com/monitoring/alerts). + +While Google has provided [documentation and examples](https://cloud.google.com/stackdriver/pricing#pricing-alerting), it is still very hard to actually estimate the cost of an Alerting Policy, let alone if you have dozens or possible hundreds of them. +Using `appe`, you can easily estimate the price of not just an individual Alerting Policy, but all of them in your entire organization using a single command. + +## Quick Start +1. Make sure you have Application Default Credentials set up correctly (See [Authentication](#authentication)) +2. Make sure you have the [recommended roles](#recommended-roles) +3. Download the [release](https://github.com/doitintl/gcp-tool-appe/releases) for your operating system and CPU architecture +4. Run `appe` on your project with `appe -p PROJECT_ID` (or see [usage](#usage) for other examples) + +## Output +`appe` can output human-readable output to the standard console output (`stdout`) or stream the results to a CSV file while it is scanning with the `--csvOutput FILENAME` flag. + +## Required Permissions +In order to get the metadata of a policy or list the existing policies within a project, you will need the following permissions: +- `monitoring.alertPolicies.get` +- `monitoring.alertPolicies.list` +These would be included in the [Monitoring AlertPolicy Viewer](https://cloud.google.com/iam/docs/understanding-roles#monitoring.alertPolicyViewer) (`roles/monitoring.alertPolicyViewer`) role. However, the metadata is not enough to estimate the price and we will need to actually execute the policy’s condition. This requires the `monitoring.timeSeries.list` permission, which is included in the [Monitoring Viewer](https://cloud.google.com/iam/docs/understanding-roles#monitoring.viewer) (`roles/monitoring.viewer`) role. +If you want to run `appe` on more than individual policies, you will also need the `resourcemanager.projects.list` permission (which is also conveniently included in the Monitoring Viewer role). If you need to recursively scan for projects (i.e. go into subfolders), you will also need the `resourcemanager.folders.list` permission. +You can also use the `--testPermissions` flag to let `appe` verify that you have the correct permissions before trying to use them in order to avoid errors in your logs. + +## Recommended Roles +We recommend that you assign the following to roles for full compatibility: +- [Monitoring Viewer](https://cloud.google.com/iam/docs/understanding-roles#monitoring.viewer) (`roles/monitoring.viewer`) +- [Browser](https://cloud.google.com/iam/docs/understanding-roles#browser) (`roles/browser`) + +## Installation +The easiest way to get and install `appe` is to download one of the pre-compiled binaries from the [releases](https://github.com/doitintl/gcp-tool-appe/releases). `appe` is a self-contained binary without any dependencies and can be run from anywhere. You do not need to download any runtime and there is no need for an installer. + +## Authentication +`appe` uses [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials) (ADC) to authenticate against the various APIs. +If you are running `appe` on a Google Cloud compute service such as Compute Engine, it will use the service's Service Account to authenticate. + +If you are running `appe` locally, the easiest way to set up ADC is to use [gcloud](https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login) by running: +```bash +gcloud auth application-default login +``` + +See also https://cloud.google.com/docs/authentication/provide-credentials-adc#local-dev. + +### macOS +Since the binary is not signed with an Apple Developer Certificate, your Mac will likely report it as untrustworthy. +There are two ways to deal with this: +1. Run `xattr -dr com.apple.quarantine ` to add the executable to the allow list (credit to [openecoacoustics.org](https://openecoacoustics.org/resources/help-centre/software/unsigned/)) +2. Compile it yourself (see below) + +### Compile appe Yourself +The easiest way to do this is to: +1. [Install Go](https://go.dev/doc/install) +2. Run `go install github.com/doitintl/gcp-tool-appe@latest` + +This will download the dependencies, compile the program and put the executable in your `$GOPATH/bin` directory (see [GOPATH](https://go.dev/wiki/GOPATH)) + +Alternatively, you can also clone the repository and run `go build` to compile it manually. + +## Usage +Using `appe` is fairly straightforward + +### Estimate the Price of Individual Policies +To estimate the price for individual policies, you can reference them directly with the `--policy` flag: +```bash +./appe --policy projects/PROJECT_ID/alertPolicies/POLICY_ID +``` +You can also specify multiple policies: +```bash +./appe --policy projects/PROJECT_ID/alertPolicies/POLICY_ID_1,projects/PROJECT_ID/alertPolicies/POLICY_ID_2 +``` + +### Estimate the Price for all Policies in a Project +To estimate the price for all policies in a project, you can specify the project either with the `--project` flag or the shorthand `-p`: +```bash +./appe -p PROJECT_ID +``` +You can also specify multiple projects: +```bash +./appe -p PROJECT_ID_1,PROJECT_ID_2 +``` + +### Estimate the Price for all Policies in all Projects in a Folder +To estimate the price of all policies in all projects in a folder, you can specify the folder ID either with the `--folder` flag or the shorthand `-f`: +```bash +./appe -f FOLDER_ID +``` +You can also specify multiple folders: +```bash +./appe -f FOLDER_ID_1,FOLDER_ID_2 +``` +Note that you will need to specify the `--recursive` or `-r` flag to also scan subfolders. + +### Estimate the Price for all Policies in all Projects in an Organization +To estimate the price of all policies in all projects in an organization, you can specify the organization ID either with the `--organization` flag or the shorthand `-o`: +```bash +./appe -o ORG_ID +``` +You can also specify multiple organizations: +```bash +./appe -o ORG_ID_1,ORG_ID_2 +``` +Note that you will need to specify the `--recursive` or `-r` flag to also scan subfolders. + +### All Flags +``` + -c, --csvOut string Path to a CSV file to redirect output to. If this is not set, human-readable output will be given on stdout. + -d, --duration duration The delta from now to go back in time for query. Default is 30 days. (default 720h0m0s) + -e, --excludeFolder strings One or more folders to exclude. Separated by ",". + -f, --folder strings One or more folders to scan. Use the "-r" flag to scan recursively. Separated by ",". + -h, --help help for appe + -i, --includeDisabled If the application should also include disabled policies. (default false) + -o, --organization strings One or more organizations to scan. Use the "-r" flag to scan recursively. Separated by ",". + --policy strings One or more alerting policies to analyze. Names must be given in full in the format "projects/PROJECT_ID/alertPolicies/POLICY_ID". Separated by ",". + -p, --project strings One or more projects to scan. Separated by ",". + -q, --quotaProject string A quota or billing project. Useful if you don't have the serviceusage.services.use permission in the target project. + -r, --recursive If parent should be scanned recursively. If this is not set, only projects at the root of the folder or organization will be scanned. (default false) + -t, --testPermissions If the application should verify that the user has the necessary permissions before processing a project. (default false) + --threads int Number of threads to use to process folders, projects and policies in parallel. (default 4) + -v, --version version for appe +``` diff --git a/cmd/monitoring.go b/cmd/monitoring.go new file mode 100644 index 0000000..1780658 --- /dev/null +++ b/cmd/monitoring.go @@ -0,0 +1,254 @@ +package cmd + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "slices" + "strings" + "time" + + "cloud.google.com/go/iam/apiv1/iampb" + monitoring "cloud.google.com/go/monitoring/apiv3/v2" + "cloud.google.com/go/monitoring/apiv3/v2/monitoringpb" + resourcemanager "cloud.google.com/go/resourcemanager/apiv3" + "cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb" + "google.golang.org/api/iterator" + monitoring_v1 "google.golang.org/api/monitoring/v1" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type policy struct { + TimeSeries int + Conditions int + ProjectId string + Name string + DisplayName string + Error string + Price float64 +} + +type pqlResponse struct { + Data struct { + Result []struct { + Values [][]any `json:"values"` + } `json:"result"` + } `json:"data"` +} + +func listProjects(ctx context.Context, projectsClient *resourcemanager.ProjectsClient, foldersClient *resourcemanager.FoldersClient, parent string, projects chan string, recursive bool, excludedFolders []string) { + if slices.Contains(excludedFolders, parent[strings.Index(parent, "/")+1:]) { + return + } + itProjects := projectsClient.ListProjects(ctx, &resourcemanagerpb.ListProjectsRequest{ + Parent: parent, + }) + for { + project, err := itProjects.Next() + if err == iterator.Done { + break + } + if err != nil { + log.Printf("Failed to list projects under %s: %v\n", parent, err) + break + } + projects <- project.ProjectId + } + if recursive { + itFolders := foldersClient.ListFolders(ctx, &resourcemanagerpb.ListFoldersRequest{ + Parent: parent, + }) + for { + folder, err := itFolders.Next() + if err == iterator.Done { + break + } + if err != nil { + log.Printf("Failed to list folders under %s: %v\n", parent, err) + break + } + listProjects(ctx, projectsClient, foldersClient, folder.Name, projects, recursive, excludedFolders) + } + } +} + +func getProjectId(alertPolicy *monitoringpb.AlertPolicy) string { + name := alertPolicy.GetName() + s1 := name[strings.Index(name, "/")+1:] + return s1[:strings.Index(s1, "/")] +} + +func verifyProjectPermissions(ctx context.Context, projectsClient *resourcemanager.ProjectsClient, projectId string, projectsTested chan string, testPermissions bool) { + if testPermissions { + permissions := []string{"monitoring.timeSeries.list", "monitoring.alertPolicies.get", "monitoring.alertPolicies.list"} + resp, err := projectsClient.TestIamPermissions(ctx, &iampb.TestIamPermissionsRequest{ + Resource: "projects/" + projectId, + Permissions: permissions, + }) + if err != nil { + log.Printf("Failed to test IAM permissions on project %s: %v\n", projectId, err) + return + } + for i := range permissions { + if !slices.Contains(resp.GetPermissions(), permissions[i]) { + log.Printf("No permission %s on %s. Skipping\n", permissions[i], projectId) + return + } + } + } + projectsTested <- projectId +} + +func listAlertPolicies(ctx context.Context, projectId string, includeDisabled bool, alertingPolicyClient *monitoring.AlertPolicyClient, policiesIn chan *monitoringpb.AlertPolicy) { + alertPoliciesIt := alertingPolicyClient.ListAlertPolicies(ctx, &monitoringpb.ListAlertPoliciesRequest{ + Name: "projects/" + projectId, + }) + for { + alertPolicy, err := alertPoliciesIt.Next() + if err == iterator.Done { + break + } + if err != nil { + log.Printf("Failed to list policies in %s: %v\n", projectId, err) + break + } + enabled := alertPolicy.GetEnabled() + if (enabled != nil && enabled.GetValue()) || includeDisabled { + policiesIn <- alertPolicy + } + } +} + +func processAlertPolicy( + ctx context.Context, + queryClient *monitoring.QueryClient, + metricClient *monitoring.MetricClient, + httpClient *http.Client, + alertPolicy *monitoringpb.AlertPolicy, + start *timestamppb.Timestamp, + end *timestamppb.Timestamp, + policiesOut chan *policy) { + projectId := getProjectId(alertPolicy) + name := "projects/" + projectId + conditions := alertPolicy.GetConditions() + policyOut := &policy{ + ProjectId: projectId, + Name: alertPolicy.GetName(), + DisplayName: alertPolicy.GetDisplayName(), + Conditions: len(conditions), + Price: 1.5 * float64(len(conditions)), + } + for i := range conditions { + mql := conditions[i].GetConditionMonitoringQueryLanguage() + pql := conditions[i].GetConditionPrometheusQueryLanguage() + threshold := conditions[i].GetConditionThreshold() + absent := conditions[i].GetConditionAbsent() + if mql != nil { + tsIt := queryClient.QueryTimeSeries(ctx, &monitoringpb.QueryTimeSeriesRequest{ + Name: name, + Query: mql.GetQuery(), + }) + for { + _, err := tsIt.Next() + if err == iterator.Done { + break + } + if err != nil { + policyOut.Error = err.Error() + break + } + // 60 (seconds) * 60 (minutes) * 24 (hours) * 30 (days) / 30 (step) * 0.35 (price) / 1000000 (per 1M) = 0.03024 (price per time series) + policyOut.Price += 0.03024 + policyOut.TimeSeries++ + } + } + if pql != nil { + seconds := pql.GetEvaluationInterval().GetSeconds() + // https://github.com/googleapis/google-api-go-client/issues/2304 + body, err := json.Marshal(&monitoring_v1.QueryRangeRequest{ + Query: pql.GetQuery(), + Start: start.AsTime().Format(time.RFC3339), + End: end.AsTime().Format(time.RFC3339), + Step: fmt.Sprintf("%ds", seconds), + }) + if err != nil { + policyOut.Error = err.Error() + continue + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("https://monitoring.googleapis.com/v1/projects/%s/location/global/prometheus/api/v1/query_range", projectId), bytes.NewReader(body)) + if err != nil { + policyOut.Error = err.Error() + continue + } + resp, err := httpClient.Do(req) + if err != nil { + policyOut.Error = err.Error() + continue + } + defer resp.Body.Close() + res, err := io.ReadAll(resp.Body) + if err != nil { + policyOut.Error = err.Error() + continue + } + pqlResp := &pqlResponse{} + err = json.Unmarshal(res, pqlResp) + if err != nil { + policyOut.Error = err.Error() + continue + } + // 60 (seconds) * 60 (minutes) * 24 (hours) * 30 (days) = 2592000 + // 2592000 * 0.35 (price) / 1000000 (per 1M) = + // 0.9072 / step * time series = price for all time series with this condition + policyOut.Price += 0.9072 / float64(seconds) * float64(len(pqlResp.Data.Result)) + policyOut.TimeSeries += len(pqlResp.Data.Result) + } + if threshold != nil || absent != nil { + tsReq := &monitoringpb.ListTimeSeriesRequest{ + Name: name, + View: monitoringpb.ListTimeSeriesRequest_HEADERS, + Interval: &monitoringpb.TimeInterval{ + EndTime: end, + StartTime: start, + }, + } + aggregations := []*monitoringpb.Aggregation{} + if threshold != nil { + tsReq.Filter = threshold.GetFilter() + aggregations = threshold.GetAggregations() + } + if absent != nil { + tsReq.Filter = absent.GetFilter() + aggregations = absent.GetAggregations() + } + if len(aggregations) > 0 { + tsReq.Aggregation = aggregations[0] + } + if len(aggregations) > 1 { + tsReq.SecondaryAggregation = aggregations[1] + } + if tsReq.Aggregation.GetCrossSeriesReducer().String() == "REDUCE_COUNT_FALSE" || tsReq.SecondaryAggregation.GetCrossSeriesReducer().String() == "REDUCE_COUNT_FALSE" { + tsReq.View = monitoringpb.ListTimeSeriesRequest_FULL + } + tsIt := metricClient.ListTimeSeries(ctx, tsReq) + for { + _, err := tsIt.Next() + if err == iterator.Done { + break + } + if err != nil { + policyOut.Error = err.Error() + break + } + // 60 (seconds) * 60 (minutes) * 24 (hours) * 30 (days) / 30 (step) * 0.35 (price) / 1000000 (per 1M) = 0.03024 (price per time series) + policyOut.Price += 0.03024 + policyOut.TimeSeries++ + } + } + } + policiesOut <- policyOut +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..6a7e0e0 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,306 @@ +package cmd + +import ( + "context" + "encoding/csv" + "log" + "os" + "strconv" + "sync" + "time" + + monitoring "cloud.google.com/go/monitoring/apiv3/v2" + "cloud.google.com/go/monitoring/apiv3/v2/monitoringpb" + resourcemanager "cloud.google.com/go/resourcemanager/apiv3" + "github.com/spf13/cobra" + "golang.org/x/oauth2/google" + "google.golang.org/api/option" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Version: "0.1", + Use: "appe", + Short: "Alerting Policy Price Estimator", + Long: `Scans for alerting policies in the specified projects, folder or orgs and approximates their cost by executing the queries defined in them against the monitoring API`, + Run: func(cmd *cobra.Command, args []string) {}, + Example: `To estimate the price for individual policies, you can reference them directly with the --policy flag: +./appe --policy projects/PROJECT_ID/alertPolicies/POLICY_ID +You can also specify multiple policies: +./appe --policy projects/PROJECT_ID/alertPolicies/POLICY_ID_1,projects/PROJECT_ID/alertPolicies/POLICY_ID_2 + +To estimate the price for all policies in a project, you can specify the project either with the --project flag or the shorthand -p: +./appe -p PROJECT_ID +You can also specify multiple projects: +./appe -p PROJECT_ID_1,PROJECT_ID_2 + +./appe -f FOLDER_ID +You can also specify multiple folders: +./appe -f FOLDER_ID_1,FOLDER_ID_2 + +To estimate the price of all policies in all projects in an organization, you can specify the organization ID either with the --organization flag or the shorthand -o: +./appe -o ORG_ID +You can also specify multiple organizations: +./appe -o ORG_ID_1,ORG_ID_2 + +Note that you will need to specify the --recursive or -r flag to also scan subfolders.`, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } + + // Parse flags + projects, err := rootCmd.Flags().GetStringSlice("project") + if err != nil { + log.Fatalln(err) + } + folders, err := rootCmd.Flags().GetStringSlice("folder") + if err != nil { + log.Fatalln(err) + } + organizations, err := rootCmd.Flags().GetStringSlice("organization") + if err != nil { + log.Fatalln(err) + } + csvOut, err := rootCmd.Flags().GetString("csvOut") + if err != nil { + log.Fatalln(err) + } + threads, err := rootCmd.Flags().GetInt64("threads") + if err != nil { + log.Fatalln(err) + } + recursive, err := rootCmd.Flags().GetBool("recursive") + if err != nil { + log.Fatalln(err) + } + testPermissions, err := rootCmd.Flags().GetBool("testPermissions") + if err != nil { + log.Fatalln(err) + } + includeDisabled, err := rootCmd.Flags().GetBool("includeDisabled") + if err != nil { + log.Fatalln(err) + } + quotaProject, err := rootCmd.Flags().GetString("quotaProject") + if err != nil { + log.Fatalln(err) + } + duration, err := rootCmd.Flags().GetDuration("duration") + if err != nil { + log.Fatalln(err) + } + excludedFolders, err := rootCmd.Flags().GetStringSlice("excludeFolder") + if err != nil { + log.Fatalln(err) + } + policies, err := rootCmd.Flags().GetStringSlice("policy") + if err != nil { + log.Fatalln(err) + } + + // Set up re-usable variables + ctx := context.Background() + now := time.Now() + end := timestamppb.Now() + start := timestamppb.New(now.Add(-duration)) + projectsIn := make(chan string, threads) + projectsTested := make(chan string, threads) + policiesIn := make(chan *monitoringpb.AlertPolicy, threads) + policiesOut := make(chan *policy, threads) + lenP := len(projects) + lenF := len(folders) + lenO := len(organizations) + lenPol := len(policies) + + // Set up API clients + alertingPolicyClient, err := monitoring.NewAlertPolicyClient(ctx, option.WithQuotaProject(quotaProject)) + if err != nil { + log.Fatalf("Failed to create alert policy client: %v", err) + } + queryClient, err := monitoring.NewQueryClient(ctx, option.WithQuotaProject(quotaProject)) + if err != nil { + log.Fatalf("Failed to create query client: %v", err) + } + metricClient, err := monitoring.NewMetricClient(ctx, option.WithQuotaProject(quotaProject)) + if err != nil { + log.Fatalf("Failed to create metric client: %v", err) + } + projectsClient, err := resourcemanager.NewProjectsClient(ctx, option.WithQuotaProject(quotaProject)) + if err != nil { + log.Fatalf("Failed to create projects client: %v", err) + } + foldersClient, err := resourcemanager.NewFoldersClient(ctx, option.WithQuotaProject(quotaProject)) + if err != nil { + log.Fatalf("Failed to create folders client: %v", err) + } + // https://github.com/googleapis/google-api-go-client/issues/2304 + // monitoring_v1Service, err := monitoring_v1.NewService(ctx, option.WithQuotaProject(quotaProject)) + // if err != nil { + // log.Fatalf("Failed to create monitoring v1 client: %v", err) + // } + httpClient, err := google.DefaultClient(ctx, monitoring.DefaultAuthScopes()...) + if err != nil { + log.Fatalf("Failed to create default http client: %v", err) + } + + // If the application was executed with the --project or -p flag, put all the projects directly in the projects channel. + // Once done, we close the projects channel because we know there won't be any more projects coming in. + if lenP > 0 { + if lenP > int(threads) { + threads = int64(lenP) + } + go func() { + for i := range projects { + projectsIn <- projects[i] + } + close(projectsIn) + }() + } + + // If the application was executed with orgs or folders, we first list the parents under them. + // Once done, we close the projects channel because we know there won't be any more projects coming in. + if lenF > 0 { + go func() { + for i := range folders { + listProjects(ctx, projectsClient, foldersClient, "folders/"+folders[i], projectsIn, recursive, excludedFolders) + } + close(projectsIn) + }() + } + if lenO > 0 { + go func() { + for i := range organizations { + listProjects(ctx, projectsClient, foldersClient, "organizations/"+organizations[i], projectsIn, recursive, excludedFolders) + } + close(projectsIn) + }() + } + + // If one or more individual policies should be analyzed, we need to first get them from the API. + // We then put them directly on the policiesIn channel, which will be processes by threads that are spawned below. + // Finally, we will close teh projectsIn channel once done, because the policiesIn channel will be closed automatically. + if lenPol > 0 { + if lenPol > int(threads) { + threads = int64(lenPol) + } + go func() { + for i := range policies { + policy, err := alertingPolicyClient.GetAlertPolicy(ctx, &monitoringpb.GetAlertPolicyRequest{ + Name: policies[i], + }) + if err != nil { + log.Fatal(err) + } + policiesIn <- policy + } + close(projectsIn) + }() + } + + // We create a wait group with the number of threads to use for parallel processing of projects + // We then spawn the threads that will verify the permissions on the projects and put them in the projectsTested channel + var wg1 sync.WaitGroup + wg1.Add(int(threads)) + for i := 0; i < int(threads); i++ { + go func() { + for project := range projectsIn { + verifyProjectPermissions(ctx, projectsClient, project, projectsTested, testPermissions) + } + wg1.Done() + }() + } + + // We create a second wait group with the number of threads to use for parallel processing of projects + // We then create the threads that will look for policies in the tested projects and put them in the policiesIn channel + var wg2 sync.WaitGroup + wg2.Add(int(threads)) + for i := 0; i < int(threads); i++ { + go func() { + for project := range projectsTested { + // processAlertingPolicies(ctx, alertingPolicyClient, queryClient, metricClient, httpClient, project, start, end, parallelPolicies, policiesOut) + listAlertPolicies(ctx, project, includeDisabled, alertingPolicyClient, policiesIn) + } + wg2.Done() + }() + } + + // These threads will loop over the found policies and execute their queries to estimate their cost + var wg3 sync.WaitGroup + wg3.Add(int(threads)) + for i := 0; i < int(threads); i++ { + go func() { + for policy := range policiesIn { + processAlertPolicy(ctx, queryClient, metricClient, httpClient, policy, start, end, policiesOut) + } + wg3.Done() + }() + } + + // We create one thread that will just wait for the other threads and close the channels in the correct order + go func() { + // We wait until all of the threads that may put projects in the projectsTested channel are done before closing it + wg1.Wait() + close(projectsTested) + // We then wait until all of the threads that are listing policies are done before closing the policiesIn channel + wg2.Wait() + close(policiesIn) + // We then wait until all of the threads that are processing policies are done before closing the policiesOut channel + wg3.Wait() + close(policiesOut) + }() + + // If the --csvOut flag was used, we create a CSV writer and write each policy as a line to the file + if csvOut != "" { + csvFile, err := os.Create(csvOut) + if err != nil { + log.Fatalf("Failed to create CSV file: %v", err) + } + defer csvFile.Close() + csvWriter := csv.NewWriter(csvFile) + err = csvWriter.Write([]string{"ProjectId", "Policy Name", "DisplayName", "Conditions", "Time Series", "Price", "Error"}) + if err != nil { + log.Fatalln("Failed writing header to file", err) + } + csvWriter.Flush() + for policy := range policiesOut { + err = csvWriter.Write([]string{policy.ProjectId, policy.Name, policy.DisplayName, strconv.Itoa(policy.Conditions), strconv.Itoa(policy.TimeSeries), strconv.FormatFloat(policy.Price, 'f', 2, 64), policy.Error}) + if err != nil { + log.Fatalln("Failed writing record to file", err) + } + csvWriter.Flush() + } + // Otherwise, the application will just output to stdout + } else { + for policy := range policiesOut { + log.Printf("Alerting Policy %s (%s) has %d condition(s) and %d time series. It will cost approximately $%f\n", policy.DisplayName, policy.Name, policy.Conditions, policy.TimeSeries, policy.Price) + } + } +} + +func init() { + rootCmd.Flags().StringP("quotaProject", "q", "", "A quota or billing project. Useful if you don't have the serviceusage.services.use permission in the target project.") + rootCmd.Flags().StringP("csvOut", "c", "", "Path to a CSV file to redirect output to. If this is not set, human-readable output will be given on stdout.") + rootCmd.Flags().StringSlice("policy", nil, "One or more alerting policies to analyze. Names must be given in full in the format \"projects/PROJECT_ID/alertPolicies/POLICY_ID\". Separated by \",\".") + rootCmd.Flags().StringSliceP("project", "p", nil, "One or more projects to scan. Separated by \",\".") + rootCmd.Flags().StringSliceP("folder", "f", nil, "One or more folders to scan. Use the \"-r\" flag to scan recursively. Separated by \",\".") + rootCmd.Flags().StringSliceP("organization", "o", nil, "One or more organizations to scan. Use the \"-r\" flag to scan recursively. Separated by \",\".") + rootCmd.Flags().StringSliceP("excludeFolder", "e", nil, "One or more folders to exclude. Separated by \",\".") + rootCmd.Flags().BoolP("testPermissions", "t", false, "If the application should verify that the user has the necessary permissions before processing a project. (default false)") + rootCmd.Flags().BoolP("includeDisabled", "i", false, "If the application should also include disabled policies. (default false)") + rootCmd.Flags().BoolP("recursive", "r", false, "If parent should be scanned recursively. If this is not set, only projects at the root of the folder or organization will be scanned. (default false)") + rootCmd.Flags().Int64("threads", 4, "Number of threads to use to process folders, projects and policies in parallel.") + rootCmd.Flags().DurationP("duration", "d", 24*time.Hour*30, "The delta from now to go back in time for query. Default is 30 days.") + rootCmd.MarkFlagsOneRequired("policy", "project", "folder", "organization") + rootCmd.MarkFlagsMutuallyExclusive("policy", "project", "recursive") + rootCmd.MarkFlagsMutuallyExclusive("policy", "testPermissions") + rootCmd.MarkFlagsMutuallyExclusive("policy", "includeDisabled") + rootCmd.MarkFlagsMutuallyExclusive("policy", "project", "excludeFolder") + rootCmd.MarkFlagsMutuallyExclusive("policy", "project", "folder", "organization") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8811ba5 --- /dev/null +++ b/go.mod @@ -0,0 +1,49 @@ +module github.com/doitintl/gcp-tool-appe + +go 1.23 + +require ( + cloud.google.com/go/iam v1.1.13 + cloud.google.com/go/monitoring v1.20.4 + cloud.google.com/go/resourcemanager v1.9.12 + github.com/spf13/cobra v1.8.1 + golang.org/x/oauth2 v0.22.0 + google.golang.org/api v0.192.0 + google.golang.org/protobuf v1.34.2 +) + +require ( + cloud.google.com/go v0.115.1 // indirect + cloud.google.com/go/auth v0.9.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect + cloud.google.com/go/compute/metadata v0.5.0 // indirect + cloud.google.com/go/longrunning v0.5.12 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/s2a-go v0.1.8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.13.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/spf13/pflag v1.0.5 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect + go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.28.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect + golang.org/x/time v0.6.0 // indirect + google.golang.org/genproto v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/grpc v1.65.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5f68827 --- /dev/null +++ b/go.sum @@ -0,0 +1,180 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ= +cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc= +cloud.google.com/go/auth v0.9.0 h1:cYhKl1JUhynmxjXfrk4qdPc6Amw7i+GC9VLflgT0p5M= +cloud.google.com/go/auth v0.9.0/go.mod h1:2HsApZBr9zGZhC9QAXsYVYaWk8kNUt37uny+XVKi7wM= +cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= +cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= +cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= +cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +cloud.google.com/go/iam v1.1.13 h1:7zWBXG9ERbMLrzQBRhFliAV+kjcRToDTgQT3CTwYyv4= +cloud.google.com/go/iam v1.1.13/go.mod h1:K8mY0uSXwEXS30KrnVb+j54LB/ntfZu1dr+4zFMNbus= +cloud.google.com/go/longrunning v0.5.12 h1:5LqSIdERr71CqfUsFlJdBpOkBH8FBCFD7P1nTWy3TYE= +cloud.google.com/go/longrunning v0.5.12/go.mod h1:S5hMV8CDJ6r50t2ubVJSKQVv5u0rmik5//KgLO3k4lU= +cloud.google.com/go/monitoring v1.20.4 h1:zwcViK7mT9SV0kzKqLOI3spRadvsmvw/R9z1MHNeC0E= +cloud.google.com/go/monitoring v1.20.4/go.mod h1:v7F/UcLRw15EX7xq565N7Ae5tnYEE28+Cl717aTXG4c= +cloud.google.com/go/resourcemanager v1.9.12 h1:p++iHmmeq9iWTia8WhNmPvBhL7MZsglQpZAYlHCguBs= +cloud.google.com/go/resourcemanager v1.9.12/go.mod h1:unouv9x3+I+6kVeE10LGM3oJ8aQrUZganWnRchitbAM= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= +github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.192.0 h1:PljqpNAfZaaSpS+TnANfnNAXKdzHM/B9bKhwRlo7JP0= +google.golang.org/api v0.192.0/go.mod h1:9VcphjvAxPKLmSxVSzPlSRXy/5ARMEw5bf58WoVXafQ= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20240814211410-ddb44dafa142 h1:oLiyxGgE+rt22duwci1+TG7bg2/L1LQsXwfjPlmuJA0= +google.golang.org/genproto v0.0.0-20240814211410-ddb44dafa142/go.mod h1:G11eXq53iI5Q+kyNOmCvnzBaxEA2Q/Ik5Tj7nqBE8j4= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/goreleaser.yml b/goreleaser.yml new file mode 100644 index 0000000..3754a7b --- /dev/null +++ b/goreleaser.yml @@ -0,0 +1,67 @@ +project_name: appe +builds: + - env: + - CGO_ENABLED=0 + goos: + - windows + - linux + - darwin + goarch: + - amd64 + - arm64 +archives: + - format: tar.gz + name_template: '{{ .ProjectName }}-{{ .Version }}_{{- if eq .Os "windows" }}win{{- else }}{{ .Os }}{{ end }}-{{ .Arch }}' + format_overrides: + - goos: windows + format: zip + files: + - LICENSE +checksum: + name_template: "{{ .ProjectName }}-{{ .Version }}_SHA256SUMS" + algorithm: sha256 +binary_signs: + - {} +signs: + - artifacts: checksum + args: + # if you are using this in a GitHub action or some other automated pipeline, you + # need to pass the batch flag to indicate its not interactive. + - "--batch" + - "--local-user" + - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key + - "--output" + - "${signature}" + - "--detach-sign" + - "${artifact}" +release: + # If you want to manually examine the release before its live, uncomment this line: + draft: true +announce: + skip: true +universal_binaries: + - name_template: "{{ .ProjectName }}" + replace: true +nfpms: + # note that this is an array of nfpm configs + - # You can change the file name of the package. + # Default: `{{ .PackageName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}` + file_name_template: "{{ .ProjectName }}-{{ .Version }}_{{ .Os }}-{{ .Arch }}" + + # Your app's maintainer (probably you). + # Default is empty. + maintainer: Hannes Hayashi + + # Template to your app's description. + # Default is empty. + description: Scans for alerting policies in the specified projects, folder or orgs and approximates their cost by executing the queries defined in them against the monitoring API. + + # Your app's license. + # Default is empty. + license: MIT + + # Formats to be generated. + formats: + - apk + - deb + - rpm diff --git a/main.go b/main.go new file mode 100644 index 0000000..fedbe34 --- /dev/null +++ b/main.go @@ -0,0 +1,29 @@ +/* +Copyright © 2024 DoiT + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package main + +import "github.com/doitintl/gcp-tool-appe/cmd" + +func main() { + cmd.Execute() +}