diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..96243be --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,102 @@ +name: Release RMK using GoReleaser + +on: + push: + branches: + - master + - release/* + +permissions: + contents: write + +env: + # AWS region of the AWS account storing images in ECR. + CORE_AWS_REGION: eu-north-1 + +jobs: + goreleaser: + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.21 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ env.CORE_AWS_REGION }} + aws-access-key-id: ${{ secrets.RMK_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.RMK_AWS_SECRET_ACCESS_KEY }} + + - name: Get and push new tag version + run: | + # constants for selecting branches + readonly GIT_BRANCH="${GITHUB_REF_NAME}" + readonly GITHUB_ORG="${GITHUB_REPOSITORY_OWNER}" + + function release() { + echo "Git commit message:" + GIT_COMMIT_MSG="$(git log -1 --pretty=format:"%s")" + echo "${GIT_COMMIT_MSG}" + + if [[ ! "${GIT_COMMIT_MSG}" =~ ^Merge\ pull\ request\ #[0-9]+\ from\ ${GITHUB_ORG}/release/(v[0-9]+\.[0-9]+\.[0-9]+)$ ]]; then + >&2 echo "Pushes to master should be done via merges of PR requests from release/vN.N.N branches only." + >&2 echo "The expected message format (will be used for parsing a release tag):" + >&2 echo "Merge pull request #N from edenlabllc/release/vN.N.N" + exit 1 + fi + + VERSION="${BASH_REMATCH[1]}" + echo "DISABLE_BLOB_RMK_RC=true" >> "${GITHUB_ENV}" + } + + case "${GIT_BRANCH}" in + release/*) + VERSION="${GIT_BRANCH#release/}-rc" + echo "DISABLE_BLOB_RMK=true" >> "${GITHUB_ENV}" + echo "Release candidate version: ${VERSION}" + git fetch --tags &> /dev/null + if (git tag | grep "${VERSION}" &> /dev/null); then + git tag --delete "${VERSION}" + git push --delete origin "${VERSION}" + fi + ;; + master) + release + ;; + esac + + echo "Configure Git user.name and user.email." + git config user.name github-actions + git config user.email github-actions@github.com + + echo "Add Git tag ${VERSION}." + git tag "${VERSION}" + git push origin "${VERSION}" -f + + - name: Run GoReleaser artifact build + uses: goreleaser/goreleaser-action@v5 + with: + distribution: goreleaser + version: v1.23.0 + args: build --clean + + - name: Copy metadata RMK release + run: | + cp ./dist/metadata.json ./bin/metadata.json + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v5 + with: + distribution: goreleaser + version: v1.23.0 + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b8fde3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.idea/ +.deps/ +.env +etc/ +dist/ +metadata.json diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..a9c12a1 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,138 @@ +project_name: rmk +release: + disable: false + ids: + - rmk + github: + owner: edenlabllc + name: rmk + name_template: '{{.ProjectName}}-v{{.Version}}' +before: + hooks: +# - go get ./... + - go mod tidy +builds: + - goos: + - linux + - darwin + goarch: + - amd64 + gomips: + - hardfloat + targets: + - linux_amd64 + - darwin_amd64 + dir: . + main: . + ldflags: + - -s -w -X main.version={{.Tag}} -X main.commit={{.ShortCommit}} -X main.date={{.Date}} + - -X main.timestamp={{.Timestamp}} -X main.name={{.Binary}} -X main.builtBy=goreleaser + - -X main.target={{.Os}}_{{.Arch}} + binary: rmk + env: + - CGO_ENABLED=0 + builder: go + gobinary: go +archives: + - id: rmk + format: binary + name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}" +blobs: + - bucket: edenlabllc-rmk + provider: s3 + region: eu-north-1 + disableSSL: true + folder: '{{ .ProjectName }}/latest-rc' + disable: '{{ envOrDefault "DISABLE_BLOB_RMK_RC" "false" }}' + ids: + - rmk + extra_files: + - glob: ./bin/metadata.json + - bucket: edenlabllc-rmk + provider: s3 + region: eu-north-1 + disableSSL: true + folder: '{{ .ProjectName }}/latest' + disable: '{{ envOrDefault "DISABLE_BLOB_RMK" "false" }}' + ids: + - rmk + extra_files: + - glob: ./bin/metadata.json + - provider: s3 + region: eu-north-1 + disableSSL: true + bucket: edenlabllc-rmk + ids: + - rmk + - provider: s3 + region: eu-north-1 + disableSSL: true + bucket: edenlabllc-rmk + folder: "{{ .ProjectName }}" + ids: + - none + extra_files: + - glob: ./bin/s3-installer +checksum: + name_template: 'checksums.txt' + algorithm: sha256 +dist: dist +gomod: + gobinary: go + env: + - GOPROXY=direct +snapshot: + name_template: 'v{{ incpatch .Version }}-next' +source: + enabled: false +changelog: + skip: true +announce: + twitter: + enabled: false + reddit: + enabled: false + slack: + enabled: true + # Message template to use while publishing. + # Defaults to `{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}` + message_template: | + *Released new version of {{ .ProjectName }}: `{{.Tag}}`* + *Release notes:* + - Code base transferred from private to public GitHub repository. + - Changed S3 bucket name for RMK artifact. + + + Installation of latest RMK version: + ``` + curl -sL "https://edenlabllc-rmk.s3.eu-north-1.amazonaws.com/rmk/s3-installer" | bash + rmk --version + ``` + Installation of specific RMK version: + ``` + curl -sL "https://edenlabllc-rmk.s3.eu-north-1.amazonaws.com/rmk/s3-installer" | bash -s -- "${RMK_VERSION}" + rmk --version + ``` + Update to latest RMK version: + ``` + rmk update + ``` + Update to the specific RMK version: + ``` + rmk update --version vX.X.X + ``` + *Actions required in project repositories after an update:* + ``` + rmk config init + ``` + + # The name of the channel that the user selected as a destination for webhook messages. + channel: '#fhir-rmk' + # Set your Webhook's username. + username: 'GoReleaser' + # Emoji to use as the icon for this message. Overrides icon_url. + icon_emoji: "package" + discord: + enabled: false + teams: + enabled: false diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/aws_provider/aws.go b/aws_provider/aws.go new file mode 100644 index 0000000..259f5af --- /dev/null +++ b/aws_provider/aws.go @@ -0,0 +1,580 @@ +package aws_provider + +import ( + "bytes" + "context" + "encoding/base64" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + ddbtype "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/aws/aws-sdk-go-v2/service/ecr" + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/aws/aws-sdk-go-v2/service/s3" + s3type "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/aws/smithy-go" + "go.uber.org/zap" + + "rmk/system" +) + +type AwsConfigure struct { + Profile string `yaml:"profile,omitempty"` + Region string `yaml:"region,omitempty"` + AccountID string `yaml:"account_id,omitempty"` + UserName string `yaml:"user_name,omitempty"` + MFADeviceSerialNumber string `yaml:"mfa_device,omitempty"` + *MFAToken `yaml:"-"` + MFAProfileCredentials aws.Credentials `yaml:"-"` +} + +type MFAToken struct { + AccessKeyId string + Expiration time.Time + SecretAccessKey string + SessionToken string +} + +func (a *AwsConfigure) AWSSharedConfigFile(profile string) []string { + return []string{system.GetHomePath(".aws", "config_"+profile)} +} + +func (a *AwsConfigure) AWSSharedCredentialsFile(profile string) []string { + return []string{system.GetHomePath(".aws", "credentials_"+profile)} +} + +func (a *AwsConfigure) errorProxy(cfg aws.Config, err error) (aws.Config, error) { + for _, val := range cfg.ConfigSources { + switch result := val.(type) { + case config.SharedConfig: + if len(result.Profile) == 0 && len(result.Region) == 0 { + return aws.Config{}, fmt.Errorf("AWS profile by name %s does not exist, will be created", a.Profile) + } + } + } + + return cfg, err +} + +// configOptions - forming custom paths to AWS credentials and profile +func (a *AwsConfigure) configOptions() []func(options *config.LoadOptions) error { + return []func(options *config.LoadOptions) error{ + config.WithSharedConfigFiles(a.AWSSharedConfigFile(a.Profile)), + config.WithSharedCredentialsFiles(a.AWSSharedCredentialsFile(a.Profile)), + config.WithSharedConfigProfile(a.Profile), + } +} + +func (a *AwsConfigure) GetUserName() error { + ctx := context.TODO() + cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) + if err != nil { + return err + } + + user, err := iam.NewFromConfig(cfg).GetUser(ctx, &iam.GetUserInput{}) + if err != nil { + return err + } + + a.UserName = aws.ToString(user.User.UserName) + + return nil +} + +func (a *AwsConfigure) GetAWSCredentials() error { + ctx := context.TODO() + cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) + if err != nil { + return err + } + + if a.MFAProfileCredentials, err = cfg.Credentials.Retrieve(ctx); err != nil { + return err + } + + return nil +} + +func (a *AwsConfigure) GetMFADevicesSerialNumbers() error { + var serialNumbers = make(map[string]string) + + if err := a.GetUserName(); err != nil { + return err + } + + if err := a.GetAWSCredentials(); err != nil { + return err + } + + ctx := context.TODO() + cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) + if err != nil { + return err + } + + mfaDevices, err := iam.NewFromConfig(cfg).ListMFADevices(ctx, + &iam.ListMFADevicesInput{UserName: aws.String(a.UserName)}) + if err != nil { + return err + } + + if len(mfaDevices.MFADevices) > 1 { + for key, val := range mfaDevices.MFADevices { + fmt.Printf("%d. - MFA Device SerialNumber: %s\n", key+1, aws.ToString(val.SerialNumber)) + serialNumbers[strconv.Itoa(key+1)] = aws.ToString(val.SerialNumber) + } + + if _, ok := serialNumbers[system.ReadStdin("number SerialNumber")]; ok { + a.MFADeviceSerialNumber = serialNumbers[system.ReadStdin("number SerialNumber")] + } else { + return fmt.Errorf("incorrectly specified number SerialNumber") + } + } else if len(mfaDevices.MFADevices) == 1 { + a.MFADeviceSerialNumber = aws.ToString(mfaDevices.MFADevices[0].SerialNumber) + } + + return nil +} + +func (a *AwsConfigure) GetMFASessionToken() error { + ctx := context.TODO() + cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) + if err != nil { + return err + } + + if err := a.GetMFADevicesSerialNumbers(); err != nil { + return err + } + + token, err := sts.NewFromConfig(cfg).GetSessionToken(ctx, &sts.GetSessionTokenInput{ + DurationSeconds: aws.Int32(43200), + SerialNumber: aws.String(a.MFADeviceSerialNumber), + TokenCode: aws.String(system.ReadStdin("TOTP")), + }) + if err != nil { + return err + } + + a.MFAToken = &MFAToken{ + AccessKeyId: aws.ToString(token.Credentials.AccessKeyId), + Expiration: aws.ToTime(token.Credentials.Expiration), + SecretAccessKey: aws.ToString(token.Credentials.SecretAccessKey), + SessionToken: aws.ToString(token.Credentials.SessionToken), + } + + return nil +} + +func (a *AwsConfigure) GetAwsConfigure(profile string) (bool, error) { + ctx := context.TODO() + cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, + config.WithSharedConfigFiles(a.AWSSharedConfigFile(profile)), + config.WithSharedCredentialsFiles(a.AWSSharedCredentialsFile(profile)), + config.WithSharedConfigProfile(profile), + )) + if err != nil { + return true, err + } + + client := sts.NewFromConfig(cfg) + identity, err := client.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}) + if err != nil { + return false, err + } + + a.Region = cfg.Region + a.AccountID = aws.ToString(identity.Account) + + return true, nil +} + +func (a *AwsConfigure) GetECRCredentials(region string) (map[string]string, error) { + ctx := context.TODO() + ecrCredentials := make(map[string]string) + + cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) + if err != nil { + return nil, err + } + + // needed for specific AWS account where ECR used + cfg.Region = region + + svc := ecr.NewFromConfig(cfg) + token, err := svc.GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{}) + if err != nil { + return nil, err + } + + authData := token.AuthorizationData[0].AuthorizationToken + data, err := base64.StdEncoding.DecodeString(*authData) + if err != nil { + return nil, err + } + + parts := strings.SplitN(string(data), ":", 2) + if len(parts) < 2 { + return nil, fmt.Errorf("it is impossible to get ECR user and password "+ + "for current AWS profile: %s", a.Profile) + } + + ecrCredentials[parts[0]] = parts[1] + + return ecrCredentials, nil +} + +func (a *AwsConfigure) CreateBucket(bucketName string) error { + var ( + respError s3.ResponseError + bucketExist *s3type.BucketAlreadyExists + bucketOwner *s3type.BucketAlreadyOwnedByYou + bucketParams s3.CreateBucketInput + bucketNotFound *s3type.NotFound + ) + + ctx := context.TODO() + cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) + if err != nil { + return err + } + + client := s3.NewFromConfig(cfg) + + if a.Region == system.RegionException { + _, err := client.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: aws.String(bucketName)}) + if err != nil { + if !errors.As(err, &bucketNotFound) { + return err + } + } else { + zap.S().Infof("S3 bucket %s already exists", bucketName) + return nil + } + + bucketParams = s3.CreateBucketInput{Bucket: aws.String(bucketName)} + } else { + bucketParams = s3.CreateBucketInput{ + Bucket: aws.String(bucketName), + CreateBucketConfiguration: &s3type.CreateBucketConfiguration{ + LocationConstraint: s3type.BucketLocationConstraint(a.Region), + }, + } + } + + resp, err := client.CreateBucket(ctx, &bucketParams) + if err != nil { + var responseError *awshttp.ResponseError + if errors.As(err, &bucketExist) { + zap.S().Infof("S3 bucket %s already exists", bucketName) + return nil + } else if errors.As(err, &bucketOwner) { + zap.S().Infof("S3 bucket %s already exists and owned by you", bucketName) + return nil + } else if errors.As(err, &responseError) && responseError.ResponseError.HTTPStatusCode() == http.StatusForbidden { + zap.S().Warnf("S3 bucket %s is not created, you don't have permissions", bucketName) + return nil + } else if errors.As(err, &respError) { + return fmt.Errorf("requestID %s, hostID %s request failure by error: %v", respError.ServiceRequestID(), + respError.ServiceHostID(), respError.Error()) + } + + return err + } + + putParams := s3.PutPublicAccessBlockInput{ + Bucket: aws.String(bucketName), + PublicAccessBlockConfiguration: &s3type.PublicAccessBlockConfiguration{ + BlockPublicAcls: aws.Bool(true), + BlockPublicPolicy: aws.Bool(true), + IgnorePublicAcls: aws.Bool(true), + RestrictPublicBuckets: aws.Bool(true), + }, + } + + if _, err := client.PutPublicAccessBlock(ctx, &putParams); err != nil { + if errors.As(err, &respError) { + return fmt.Errorf("requestID %s, hostID %s request failure by error: %v", + respError.ServiceRequestID(), respError.ServiceHostID(), respError.Error()) + } + + return err + } + + zap.S().Infof("created S3 bucket: %s - %s", bucketName, *resp.Location) + + return nil +} + +func (a *AwsConfigure) CreateDynamoDBTable(tableName string) error { + ctx := context.TODO() + cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) + if err != nil { + return err + } + + client := dynamodb.NewFromConfig(cfg) + dTableParams := &dynamodb.CreateTableInput{ + AttributeDefinitions: []ddbtype.AttributeDefinition{ + { + AttributeName: aws.String("LockID"), + AttributeType: ddbtype.ScalarAttributeTypeS, + }, + }, + KeySchema: []ddbtype.KeySchemaElement{ + { + AttributeName: aws.String("LockID"), + KeyType: ddbtype.KeyTypeHash, + }, + }, + TableName: aws.String(tableName), + BillingMode: ddbtype.BillingModePayPerRequest, + } + + resp, err := client.CreateTable(ctx, dTableParams) + if err != nil { + var ( + tableExist *ddbtype.ResourceInUseException + accessDenied smithy.APIError + operation *smithy.OperationError + ) + + if errors.As(err, &tableExist) { + zap.S().Infof("DynamoDB table %s already exists", tableName) + return nil + } else if errors.As(err, &accessDenied) && errors.As(err, &operation) && + operation.Operation() == "CreateTable" && accessDenied.ErrorCode() == "AccessDeniedException" { + zap.S().Warnf("DynamoDB table %s is not created, you don't have permissions", tableName) + return nil + } + + return err + } + + zap.S().Infof("created DynamoDB table: %s - %s", tableName, *resp.TableDescription.TableArn) + + return nil +} + +func (a *AwsConfigure) BucketKeyExists(region, bucketName, key string) (bool, error) { + if len(bucketName) == 0 { + return false, nil + } + + ctx := context.TODO() + cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) + if err != nil { + return false, err + } + + // needed for specific AWS account where S3 used + if len(region) > 0 { + cfg.Region = region + } + + client := s3.NewFromConfig(cfg) + _, err = client.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(key), + }) + if err != nil { + var responseError *awshttp.ResponseError + if errors.As(err, &responseError) && responseError.ResponseError.HTTPStatusCode() == http.StatusNotFound { + return false, nil + } + + return false, err + } + + return true, nil +} + +// S3ListObjectsAPI defines the interface for the ListObjectsV2 function. +// We use this interface to test the function using a mocked service. +type S3ListObjectsAPI interface { + ListObjectsV2(ctx context.Context, + params *s3.ListObjectsV2Input, + optFns ...func(*s3.Options)) (*s3.ListObjectsV2Output, error) +} + +// GetObjects retrieves the objects in an Amazon Simple Storage Service (Amazon S3) bucket +// Inputs: +// +// c is the context of the method call, which includes the AWS Region +// api is the interface that defines the method call +// input defines the input arguments to the service call. +// +// Output: +// +// If success, a ListObjectsV2Output object containing the result of the service call and nil +// Otherwise, nil and an error from the call to ListObjectsV2 +func GetObjects(c context.Context, api S3ListObjectsAPI, input *s3.ListObjectsV2Input) (*s3.ListObjectsV2Output, error) { + return api.ListObjectsV2(c, input) +} + +func (a *AwsConfigure) DownloadFromBucket(region, bucketName, localDir, filePrefix string) error { + var noSuchBucket *s3type.NoSuchBucket + + downloadToFile := func(downloader *manager.Downloader, targetDirectory, bucket, key string) error { + // Create the directories in the path + file := filepath.Join(targetDirectory, key) + if err := os.MkdirAll(filepath.Dir(file), 0775); err != nil { + return err + } + + fd, err := os.Create(file) + if err != nil { + return err + } + defer func(fd *os.File) { + err := fd.Close() + if err != nil { + + } + }(fd) + + // Download the file using the AWS SDK for Go + zap.S().Infof("downloading s3://%s/%s to %s", bucket, key, file) + _, err = downloader.Download(context.TODO(), fd, &s3.GetObjectInput{Bucket: &bucket, Key: &key}) + + return err + } + + ctx := context.TODO() + cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) + if err != nil { + return err + } + + // needed for specific AWS account where S3 used + if len(region) > 0 { + cfg.Region = region + } + + client := s3.NewFromConfig(cfg) + m := manager.NewDownloader(client) + paginator := s3.NewListObjectsV2Paginator(client, &s3.ListObjectsV2Input{Bucket: aws.String(bucketName)}) + + for paginator.HasMorePages() { + page, err := paginator.NextPage(context.TODO()) + if err != nil { + if errors.As(err, &noSuchBucket) { + return fmt.Errorf("specified bucket %s does not exist", bucketName) + } + + return err + } + + for _, obj := range page.Contents { + if strings.HasPrefix(aws.ToString(obj.Key), filePrefix) { + if err := downloadToFile(m, localDir, bucketName, aws.ToString(obj.Key)); err != nil { + return err + } + } + } + } + + input := &s3.ListObjectsV2Input{ + Bucket: aws.String(bucketName), + } + + resp, err := GetObjects(context.TODO(), client, input) + if err != nil { + return err + } + + if len(resp.Contents) == 0 { + zap.S().Warnf("S3 bucket %s is empty, files do not exist", bucketName) + return nil + } + + return nil +} + +func (a *AwsConfigure) GetFileData(bucketName, key string) ([]byte, error) { + var client *s3.Client + ctx := context.TODO() + if len(a.Profile) > 0 { + cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) + if err != nil { + return nil, err + } + + client = s3.NewFromConfig(cfg) + } else { + client = s3.NewFromConfig(aws.Config{Region: a.Region}) + } + + input := &s3.GetObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(key), + } + + resp, err := client.GetObject(ctx, input) + if err != nil { + return nil, err + } + + return io.ReadAll(resp.Body) +} + +func (a *AwsConfigure) UploadToBucket(bucketName, localDir, pattern string) error { + ctx := context.TODO() + cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) + if err != nil { + return err + } + + uploader := manager.NewUploader(s3.NewFromConfig(cfg)) + + match, err := system.WalkMatch(localDir, pattern) + if err != nil { + return err + } + + for _, path := range match { + if filepath.Base(path) != system.SopsAgeKeyFile { + data, err := os.ReadFile(path) + if err != nil { + return err + } + + result, err := uploader.Upload(context.TODO(), &s3.PutObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(filepath.Base(path)), + Body: bytes.NewReader(data), + }) + if err != nil { + var responseError *awshttp.ResponseError + if errors.As(err, &responseError) && responseError.ResponseError.HTTPStatusCode() == http.StatusNotFound { + return fmt.Errorf("specified bucket %s does not exist", bucketName) + } + + if errors.As(err, &responseError) && responseError.ResponseError.HTTPStatusCode() == http.StatusForbidden { + return fmt.Errorf("you are not permitted to upload SOPS age keys for specified bucket %s, "+ + "access denied", bucketName) + } + + return err + } + + zap.S().Infof("uploading %s... to %s", path, result.Location) + } + } + + return nil +} diff --git a/bin/s3-installer b/bin/s3-installer new file mode 100755 index 0000000..43c8498 --- /dev/null +++ b/bin/s3-installer @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +S3_BUCKET_NAME=edenlabllc-rmk +S3_URL="https://${S3_BUCKET_NAME}.s3.eu-north-1.amazonaws.com/rmk" +VERSION="${1:-latest}" +LOCAL_BIN_DIR="${HOME}"/.local/bin + +case "${OSTYPE}" in + darwin*) + FILE=rmk_darwin_amd64 + ;; + linux*) + FILE=rmk_linux_amd64 + ;; + *) + FILE=rmk_linux_amd64 + ;; +esac + +mkdir -p "${LOCAL_BIN_DIR}" + +curl -fL "${S3_URL}/${VERSION}/${FILE}" > "${LOCAL_BIN_DIR}/rmk" && chmod +x "${LOCAL_BIN_DIR}/rmk" + +# Linux case, you need to create a symlink manually due to +# restrictions on permissions in the /usr/local/bin directory +if [[ ! -f /usr/local/bin/rmk ]]; then + if ! (ln -s "${LOCAL_BIN_DIR}"/rmk /usr/local/bin/rmk &> /dev/null) then + printf "\nWARNING: The symlink was not created automatically, please complete the installation by running the command: %s\n" \ + "sudo ln -s ${LOCAL_BIN_DIR}/rmk /usr/local/bin/rmk" + exit 0 + fi +fi diff --git a/commands/cluster_category.go b/commands/cluster_category.go new file mode 100644 index 0000000..454ba39 --- /dev/null +++ b/commands/cluster_category.go @@ -0,0 +1,529 @@ +package commands + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/urfave/cli/v2" + "go.uber.org/zap" + + "rmk/config" + "rmk/system" +) + +type StateRunner interface { + clusterStateDelete() error + clusterStateList() error + clusterStateRefresh() error +} + +type ClusterCommands struct { + Conf *config.Config + Ctx *cli.Context + SpecCMDs []*system.SpecCMD + PlanFile string + WorkDir string +} + +func (cc *ClusterCommands) clusterRootDir() (string, error) { + for _, provider := range cc.Conf.Clusters { + if strings.HasPrefix(provider.Name, cc.Conf.ClusterProvider) { + return provider.DstPath, nil + } + } + + return "", fmt.Errorf("destination path for cluster provider %s not found", cc.Conf.ClusterProvider) +} + +func (cc *ClusterCommands) awsEks() *system.SpecCMD { + return &system.SpecCMD{ + Args: []string{"eks", "--region", + cc.Conf.Region, + "update-kubeconfig", + "--name", + cc.Conf.Name + "-eks", + "--profile", + cc.Conf.Profile, + }, + Command: "aws", + Ctx: cc.Ctx.Context, + Dir: cc.WorkDir, + Debug: true, + } +} + +func (cc *ClusterCommands) runBatchCMD() error { + if err := os.Unsetenv("AWS_PROFILE"); err != nil { + return err + } + + for _, val := range cc.SpecCMDs { + val.Envs = []string{ + "AWS_PROFILE=" + cc.Conf.Profile, + "AWS_CONFIG_FILE=" + strings.Join(cc.Conf.AWSSharedConfigFile(cc.Conf.Profile), ""), + "AWS_SHARED_CREDENTIALS_FILE=" + strings.Join(cc.Conf.AWSSharedCredentialsFile(cc.Conf.Profile), ""), + } + if err := val.AddEnv(); err != nil { + return err + } + + if err := val.ExecCMD(); err != nil { + if val.Debug { + zap.S().Debugf("command: %s", val.CommandStr) + zap.S().Debugf("path: %s", val.Dir) + for _, v := range val.Envs { + zap.S().Debugf("env: %s", v) + } + } + + return err + } + + if val.Debug { + zap.S().Debugf("command: %s", val.CommandStr) + zap.S().Debugf("path: %s", val.Dir) + for _, v := range val.Envs { + zap.S().Debugf("env: %s", v) + } + } + } + + return nil +} + +func (cc *ClusterCommands) initialize() *system.SpecCMD { + args := []string{ + "init", + "-backend=true", + "-backend-config=region=" + cc.Conf.Region, + "-backend-config=bucket=" + cc.Conf.Terraform.BucketName, + "-backend-config=key=" + cc.Conf.Terraform.BucketKey, + } + if cc.Conf.ClusterProvisionerSL { + args = append(args, "-backend-config=dynamodb_table="+cc.Conf.Terraform.DDBTableName) + } + + args = append(args, "-reconfigure") + return &system.SpecCMD{ + Args: args, + Command: "terraform", + Ctx: cc.Ctx.Context, + Dir: cc.WorkDir, + Debug: true, + } +} + +func (cc *ClusterCommands) validate() *system.SpecCMD { + return &system.SpecCMD{ + Args: []string{"validate"}, + Command: "terraform", + Ctx: cc.Ctx.Context, + Dir: cc.WorkDir, + Debug: true, + } +} + +func (cc *ClusterCommands) workspace(args ...string) *system.SpecCMD { + return &system.SpecCMD{ + Args: append([]string{"workspace"}, args...), + Command: "terraform", + Ctx: cc.Ctx.Context, + Dir: cc.WorkDir, + Debug: true, + } +} + +func (cc *ClusterCommands) output(args ...string) *system.SpecCMD { + return &system.SpecCMD{ + Args: append([]string{"output"}, args...), + Command: "terraform", + Ctx: cc.Ctx.Context, + Dir: cc.WorkDir, + DisableStdOut: true, + Debug: false, + } +} + +func (cc *ClusterCommands) destroy() *system.SpecCMD { + return &system.SpecCMD{ + Args: []string{"destroy", "-auto-approve", + "-var=aws_account_id=" + cc.Conf.AccountID, + "-var=cloudflare_api_token=" + cc.Conf.CloudflareToken, + "-var=name=" + cc.Conf.Name, + "-var=region=" + cc.Conf.Region, + "-var=root_domain=" + cc.Conf.RootDomain, + "-var=terraform_bucket_key=" + cc.Conf.Terraform.BucketKey, + "-var=terraform_bucket_name=" + cc.Conf.Terraform.BucketName, + }, + Command: "terraform", + Ctx: cc.Ctx.Context, + Dir: cc.WorkDir, + Debug: true, + } +} + +func (cc *ClusterCommands) plan() *system.SpecCMD { + return &system.SpecCMD{ + Args: []string{ + "plan", + "-out=" + cc.PlanFile, + "-var=aws_account_id=" + cc.Conf.AccountID, + "-var=cloudflare_api_token=" + cc.Conf.CloudflareToken, + "-var=name=" + cc.Conf.Name, + "-var=region=" + cc.Conf.Region, + "-var=root_domain=" + cc.Conf.RootDomain, + "-var=terraform_bucket_key=" + cc.Conf.Terraform.BucketKey, + "-var=terraform_bucket_name=" + cc.Conf.Terraform.BucketName, + }, + Command: "terraform", + Ctx: cc.Ctx.Context, + Dir: cc.WorkDir, + Debug: true, + } +} + +func (cc *ClusterCommands) apply() *system.SpecCMD { + return &system.SpecCMD{ + Args: []string{"apply", cc.PlanFile}, + Command: "terraform", + Ctx: cc.Ctx.Context, + Dir: cc.WorkDir, + Debug: true, + } +} + +func (cc *ClusterCommands) listResources() *system.SpecCMD { + return &system.SpecCMD{ + Args: []string{"state", "list"}, + Command: "terraform", + Ctx: cc.Ctx.Context, + Dir: cc.WorkDir, + Debug: true, + } +} + +func (cc *ClusterCommands) refresh() *system.SpecCMD { + return &system.SpecCMD{ + Args: []string{"refresh", + "-var=aws_account_id=" + cc.Conf.AccountID, + "-var=cloudflare_api_token=" + cc.Conf.CloudflareToken, + "-var=name=" + cc.Conf.Name, + "-var=region=" + cc.Conf.Region, + "-var=root_domain=" + cc.Conf.RootDomain, + "-var=terraform_bucket_key=" + cc.Conf.Terraform.BucketKey, + "-var=terraform_bucket_name=" + cc.Conf.Terraform.BucketName, + }, + Command: "terraform", + Ctx: cc.Ctx.Context, + Dir: cc.WorkDir, + Debug: true, + } +} + +func (cc *ClusterCommands) state(args ...string) *system.SpecCMD { + return &system.SpecCMD{ + Args: append([]string{"state"}, args...), + Command: "terraform", + Ctx: cc.Ctx.Context, + Dir: cc.WorkDir, + Debug: true, + } +} + +func (cc *ClusterCommands) clusterContext() error { + cc.SpecCMDs = append(cc.SpecCMDs, cc.awsEks()) + return cc.runBatchCMD() +} + +func (cc *ClusterCommands) clusterDestroy() error { + checkWorkspace, err := cc.Conf.BucketKeyExists("", cc.Conf.Terraform.BucketName, "env:/"+cc.Conf.Name+"/tf.tfstate") + if err != nil { + return err + } + + if checkWorkspace { + cc.SpecCMDs = append(cc.SpecCMDs, cc.initialize(), cc.validate(), cc.workspace("select", cc.Conf.Name)) + if err := cc.runBatchCMD(); err != nil { + return err + } + + destroy := cc.destroy() + + match, err := system.WalkMatch( + system.GetPwdPath(system.TenantValuesDIR, "clusters", system.AWSClusterProvider, cc.Conf.Environment), + "*."+system.TerraformVarsExt, + ) + if err != nil { + return err + } + + for _, val := range match { + destroy.Args = append(destroy.Args, "-var-file="+val) + } + + if err := os.RemoveAll(cc.PlanFile); err != nil { + return err + } + + cc.SpecCMDs = append([]*system.SpecCMD{}, destroy, cc.workspace("select", "default"), + cc.workspace("delete", cc.Conf.Name)) + + return cc.runBatchCMD() + } else { + zap.S().Infof("Terraform cluster in workspace %s already deleted or not created", cc.Conf.Name) + return nil + } +} + +func (cc *ClusterCommands) clusterList() error { + cc.SpecCMDs = append(cc.SpecCMDs, cc.initialize(), cc.workspace("list")) + return cc.runBatchCMD() +} + +func (cc *ClusterCommands) clusterProvision() error { + var workspace *system.SpecCMD + + if err := os.MkdirAll(filepath.Join(cc.WorkDir, "plans"), 0755); err != nil { + zap.S().Fatal(err) + } + + checkWorkspace, err := cc.Conf.BucketKeyExists("", cc.Conf.Terraform.BucketName, "env:/"+cc.Conf.Name+"/tf.tfstate") + if err != nil { + zap.S().Fatal(err) + } + + if checkWorkspace { + workspace = cc.workspace("select", cc.Conf.Name) + } else { + workspace = cc.workspace("new", cc.Conf.Name) + } + + plan := cc.plan() + + match, err := system.WalkMatch( + system.GetPwdPath(system.TenantValuesDIR, "clusters", system.AWSClusterProvider, cc.Conf.Environment), + "*."+system.TerraformVarsExt, + ) + + for _, val := range match { + plan.Args = append(plan.Args, "-var-file="+val) + } + + if cc.Ctx.Bool("plan") { + cc.SpecCMDs = append(cc.SpecCMDs, cc.initialize(), cc.validate(), workspace, plan) + return cc.runBatchCMD() + } + + cc.SpecCMDs = append(cc.SpecCMDs, cc.initialize(), cc.validate(), workspace, plan, cc.apply()) + if err := cc.runBatchCMD(); err != nil { + return err + } + + rc := &ReleaseCommands{ + Conf: cc.Conf, + Ctx: cc.Ctx, + WorkDir: system.GetPwdPath(""), + UpdateContext: true, + } + + return rc.releaseKubeContext() +} + +func (cc *ClusterCommands) clusterStateDelete() error { + cc.SpecCMDs = append(cc.SpecCMDs, cc.state("rm", cc.Ctx.String("resource-address"))) + + return cc.runBatchCMD() +} + +func (cc *ClusterCommands) clusterStateList() error { + cc.SpecCMDs = append(cc.SpecCMDs, cc.state(cc.Ctx.Command.Name)) + + return cc.runBatchCMD() +} + +func (cc *ClusterCommands) clusterStateRefresh() error { + var workspace *system.SpecCMD + + checkWorkspace, err := cc.Conf.BucketKeyExists("", cc.Conf.Terraform.BucketName, "env:/"+cc.Conf.Name+"/tf.tfstate") + if err != nil { + zap.S().Fatal(err) + } + + if checkWorkspace { + workspace = cc.workspace("select", cc.Conf.Name) + } else { + workspace = cc.workspace("new", cc.Conf.Name) + } + + refresh := cc.refresh() + match, err := system.WalkMatch( + system.GetPwdPath(system.TenantValuesDIR, "clusters", system.AWSClusterProvider, cc.Conf.Environment), + "*."+system.TerraformVarsExt, + ) + if err != nil { + return err + } + + for _, val := range match { + refresh.Args = append(refresh.Args, "-var-file="+val) + } + + cc.SpecCMDs = append(cc.SpecCMDs, cc.initialize(), cc.validate(), workspace, refresh) + + return cc.runBatchCMD() +} + +func clusterDestroyAction(conf *config.Config) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateArtifactModeDefault(c, ""); err != nil { + return err + } + + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { + return err + } + + cc := &ClusterCommands{ + Conf: conf, + Ctx: c, + } + + if pkgDst, err := cc.clusterRootDir(); err != nil { + return err + } else { + cc.WorkDir = filepath.Join(pkgDst, "terraform") + } + + cc.PlanFile = filepath.Join(cc.WorkDir, "plans", conf.Name+"__"+conf.Environment+".tfplan") + + return cc.clusterDestroy() + } +} + +func clusterListAction(conf *config.Config) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateArtifactModeDefault(c, ""); err != nil { + return err + } + + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { + return err + } + + cc := &ClusterCommands{ + Conf: conf, + Ctx: c, + } + + if pkgDst, err := cc.clusterRootDir(); err != nil { + return err + } else { + cc.WorkDir = filepath.Join(pkgDst, "terraform") + } + + return cc.clusterList() + } +} + +func clusterProvisionAction(conf *config.Config) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateArtifactModeDefault(c, ""); err != nil { + return err + } + + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + if err := resolveDependencies(conf.InitConfig(true), c, false); err != nil { + return err + } + + cc := &ClusterCommands{ + Conf: conf, + Ctx: c, + } + + if pkgDst, err := cc.clusterRootDir(); err != nil { + return err + } else { + cc.WorkDir = filepath.Join(pkgDst, "terraform") + } + + cc.PlanFile = filepath.Join(cc.WorkDir, "plans", conf.Name+"__"+conf.Environment+".tfplan") + + if err := cc.clusterProvision(); err != nil { + return err + } + + if err := conf.GetTerraformOutputs(); err != nil { + return err + } + + return conf.CreateConfigFile() + } +} + +func clusterStateAction(conf *config.Config, action func(stateRunner StateRunner) error) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateArtifactModeDefault(c, ""); err != nil { + return err + } + + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { + return err + } + + cc := &ClusterCommands{ + Conf: conf, + Ctx: c, + } + + if pkgDst, err := cc.clusterRootDir(); err != nil { + return err + } else { + cc.WorkDir = filepath.Join(pkgDst, "terraform") + } + + return action(cc) + } +} + +func clusterSwitchAction(conf *config.Config) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateArtifactModeDefault(c, ""); err != nil { + return err + } + + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { + return err + } + + rc := &ReleaseCommands{ + Conf: conf, + Ctx: c, + WorkDir: system.GetPwdPath(""), + UpdateContext: c.Bool("force"), + } + + return rc.releaseKubeContext() + } +} diff --git a/commands/commands.go b/commands/commands.go new file mode 100644 index 0000000..5c7c1b9 --- /dev/null +++ b/commands/commands.go @@ -0,0 +1,584 @@ +package commands + +import ( + "sort" + + "github.com/urfave/cli/v2" + "github.com/urfave/cli/v2/altsrc" + "go.uber.org/zap" + + "rmk/aws_provider" + "rmk/config" + "rmk/git_handler" + "rmk/system" +) + +type Flags map[string][]cli.Flag + +func Commands() []*cli.Command { + conf := &config.Config{AwsConfigure: &aws_provider.AwsConfigure{}} + gitSpec := &git_handler.GitSpec{ + DefaultBranches: []string{ + git_handler.DefaultDevelop, + git_handler.DefaultStaging, + git_handler.DefaultProduction, + }, + } + flags := Flags{ + "clusterCRLogin": flagsClusterCRLogin(), + "clusterK3DCreate": flagsClusterK3DCreate(), + "clusterK3DImport": flagsClusterK3DImport(), + "clusterPlan": flagsClusterPlan(), + "clusterStateDelete": flagsClusterStateDelete(), + "clusterSwitch": flagsClusterSwitch(), + "config": flagsConfig(), + "configList": flagsConfigList(), + "hidden": flagsHidden(), + "projectGenerate": flagsProjectGenerate(), + "projectUpdate": flagsProjectUpdate(), + "releaseHelmfile": flagsReleaseHelmfile(false), + "releaseHelmfileWithOutput": flagsReleaseHelmfile(true), + "releaseRollback": flagsReleaseRollback(), + "releaseUpdate": flagsReleaseUpdate(), + "secretGenerate": flagsSecretGenerate(), + "secretManager": flagsSecretManager(), + "update": flagsUpdate(), + } + + for key := range flags { + sort.Sort(cli.FlagsByName(flags[key])) + } + + return []*cli.Command{ + { + Name: "completion", + Usage: "Completion management", + Subcommands: []*cli.Command{ + { + Name: "zsh", + Usage: "View Zsh completion scripts", + Description: system.CompletionZshDescription, + Aliases: []string{"z"}, + Category: "completion", + Action: completionAction(), + }, + }, + }, + { + Name: "config", + Usage: "Configuration management", + Subcommands: []*cli.Command{ + { + Name: "init", + Usage: "Initialize configuration for current tenant and selected environment", + Aliases: []string{"i"}, + Before: initInputSourceWithContext(gitSpec, flags["config"]), + Flags: flags["config"], + Category: "config", + Action: configInitAction(conf, gitSpec), + }, + { + Name: "delete", + Usage: "Delete configuration for selected environment", + Aliases: []string{"d"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + Category: "config", + BashComplete: system.ShellCompleteCustomOutput, + Action: configDeleteAction(conf), + }, + { + Name: "list", + Usage: "List available configurations for current tenant", + Aliases: []string{"l"}, + Flags: flags["configList"], + Category: "config", + BashComplete: system.ShellCompleteCustomOutput, + Action: configListAction(conf, gitSpec), + }, + { + Name: "view", + Usage: "View configuration for selected environment", + Aliases: []string{"v"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + Category: "config", + BashComplete: system.ShellCompleteCustomOutput, + Action: configViewAction(conf), + }, + }, + }, + { + Name: "cluster", + Usage: "Cluster management", + Subcommands: []*cli.Command{ + { + Name: "container-registry", + Usage: "Container registry management", + Aliases: []string{"c"}, + Category: "cluster", + Subcommands: []*cli.Command{ + { + Name: "login", + Usage: "Log in to container registry", + Before: readInputSourceWithContext(gitSpec, conf, flags["clusterCRLogin"]), + Flags: flags["clusterCRLogin"], + Category: "container-registry", + BashComplete: system.ShellCompleteCustomOutput, + Action: containerRegistryAction(conf, DockerRunner.dockerLogin), + }, + { + Name: "logout", + Usage: "Log out from container registry", + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + Category: "container-registry", + BashComplete: system.ShellCompleteCustomOutput, + Action: containerRegistryAction(conf, DockerRunner.dockerLogout), + }, + }, + }, + { + Name: "destroy", + Usage: "Destroy AWS cluster using Terraform", + Aliases: []string{"d"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + Category: "cluster", + BashComplete: system.ShellCompleteCustomOutput, + Action: clusterDestroyAction(conf), + }, + { + Name: "list", + Usage: "List all Terraform available workspaces", + Aliases: []string{"l"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + Category: "cluster", + BashComplete: system.ShellCompleteCustomOutput, + Action: clusterListAction(conf), + }, + { + Name: "k3d", + Usage: "K3D cluster management", + Aliases: []string{"k"}, + Category: "cluster", + Subcommands: []*cli.Command{ + { + Name: "create", + Usage: "Create K3D cluster", + Aliases: []string{"c"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["clusterK3DCreate"]), + Flags: flags["clusterK3DCreate"], + Category: "k3d", + BashComplete: system.ShellCompleteCustomOutput, + Action: K3DCreateAction(conf), + }, + { + Name: "delete", + Usage: "Delete K3D cluster", + Aliases: []string{"d"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + Category: "k3d", + BashComplete: system.ShellCompleteCustomOutput, + Action: K3DAction(conf, K3DRunner.createDeleteK3DCluster), + }, + { + Name: "import", + Usage: "Import images from docker to K3D cluster", + Aliases: []string{"i"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["clusterK3DImport"]), + Flags: flags["clusterK3DImport"], + Category: "k3d", + BashComplete: system.ShellCompleteCustomOutput, + Action: K3DAction(conf, K3DRunner.importImageToK3DCluster), + }, + { + Name: "list", + Usage: "List K3D clusters", + Aliases: []string{"l"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + Category: "k3d", + BashComplete: system.ShellCompleteCustomOutput, + Action: K3DAction(conf, K3DRunner.listK3DClusters), + }, + { + Name: "start", + Usage: "Start K3D cluster", + Aliases: []string{"s"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + BashComplete: system.ShellCompleteCustomOutput, + Category: "k3d", + Action: K3DAction(conf, K3DRunner.startStopK3DCluster), + }, + { + Name: "stop", + Usage: "Stop K3D cluster", + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + Category: "k3d", + BashComplete: system.ShellCompleteCustomOutput, + Action: K3DAction(conf, K3DRunner.startStopK3DCluster), + }, + }, + }, + { + Name: "provision", + Usage: "Provision AWS cluster using Terraform", + Aliases: []string{"p"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["clusterPlan"]), + Flags: flags["clusterPlan"], + Category: "cluster", + BashComplete: system.ShellCompleteCustomOutput, + Action: clusterProvisionAction(conf), + }, + { + Name: "state", + Usage: "State cluster management using Terraform", + Aliases: []string{"t"}, + Category: "cluster", + Subcommands: []*cli.Command{ + { + Name: "delete", + Usage: "Delete resource from Terraform state", + Aliases: []string{"d"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["clusterStateDelete"]), + Flags: flags["clusterStateDelete"], + Category: "state", + BashComplete: system.ShellCompleteCustomOutput, + Action: clusterStateAction(conf, StateRunner.clusterStateDelete), + }, + { + Name: "list", + Usage: "List resources from Terraform state", + Aliases: []string{"l"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + Category: "state", + BashComplete: system.ShellCompleteCustomOutput, + Action: clusterStateAction(conf, StateRunner.clusterStateList), + }, + { + Name: "refresh", + Usage: "Update state file for AWS cluster using Terraform", + Aliases: []string{"r"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + Category: "state", + BashComplete: system.ShellCompleteCustomOutput, + Action: clusterStateAction(conf, StateRunner.clusterStateRefresh), + }, + }, + }, + { + Name: "switch", + Usage: "Switch Kubernetes context for tenant cluster", + Aliases: []string{"s"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["clusterSwitch"]), + Flags: flags["clusterSwitch"], + Category: "cluster", + BashComplete: system.ShellCompleteCustomOutput, + Action: clusterSwitchAction(conf), + }, + }, + }, + { + Name: "doc", + Usage: "Documentation management", + Subcommands: []*cli.Command{ + { + Name: "generate", + Usage: "Generate documentation by commands and flags in Markdown format", + Aliases: []string{"g"}, + Category: "doc", + Action: docGenerateAction(), + }, + }, + }, + { + Name: "project", + Usage: "Project management", + Subcommands: []*cli.Command{ + { + Name: "generate", + Usage: "Generate project directories and files structure", + Aliases: []string{"g"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["projectGenerate"]), + Flags: flags["projectGenerate"], + Category: "project", + BashComplete: system.ShellCompleteCustomOutput, + Action: projectGenerateAction(conf, gitSpec), + }, + { + Name: "update", + Usage: "Update project file with specific dependencies version", + Aliases: []string{"u"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["projectUpdate"]), + Flags: flags["projectUpdate"], + Category: "project", + BashComplete: system.ShellCompleteCustomOutput, + Action: projectUpdateAction(conf, gitSpec), + }, + }, + }, + { + Name: "release", + Usage: "Release components list from state file (Helmfile)", + Subcommands: []*cli.Command{ + { + Name: "build", + Usage: "Build releases", + Aliases: []string{"b"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["releaseHelmfile"]), + Flags: flags["releaseHelmfile"], + Category: "release", + BashComplete: system.ShellCompleteCustomOutput, + Action: releaseHelmfileAction(conf), + }, + { + Name: "destroy", + Usage: "Destroy releases", + Aliases: []string{"d"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["releaseHelmfile"]), + Flags: flags["releaseHelmfileWithOutput"], + Category: "release", + BashComplete: system.ShellCompleteCustomOutput, + Action: releaseHelmfileAction(conf), + }, + { + Name: "list", + Usage: "List releases", + Aliases: []string{"l"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["releaseHelmfileWithOutput"]), + Flags: flags["releaseHelmfileWithOutput"], + Category: "release", + BashComplete: system.ShellCompleteCustomOutput, + Action: releaseHelmfileAction(conf), + }, + { + Name: "rollback", + Usage: "Rollback specific releases to latest stable state", + Aliases: []string{"r"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["releaseRollback"]), + Flags: flags["releaseRollback"], + Category: "release", + BashComplete: system.ShellCompleteCustomOutput, + Action: releaseRollbackAction(conf), + }, + { + Name: "sync", + Usage: "Sync releases", + Aliases: []string{"s"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["releaseHelmfile"]), + Flags: flags["releaseHelmfile"], + Category: "release", + BashComplete: system.ShellCompleteCustomOutput, + Action: releaseHelmfileAction(conf), + }, + { + Name: "template", + Usage: "Template releases", + Aliases: []string{"t"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["releaseHelmfile"]), + Flags: flags["releaseHelmfile"], + Category: "release", + BashComplete: system.ShellCompleteCustomOutput, + Action: releaseHelmfileAction(conf), + }, + { + Name: "update", + Usage: "Update releases file with specific environment values", + Aliases: []string{"u"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["releaseUpdate"]), + Flags: flags["releaseUpdate"], + Category: "release", + BashComplete: system.ShellCompleteCustomOutput, + Action: releaseUpdateAction(conf, gitSpec), + }, + }, + }, + { + Name: "secret", + Usage: "secrets management", + Subcommands: []*cli.Command{ + { + Name: "manager", + Usage: "batch secrets management", + Aliases: []string{"m"}, + Category: "secret", + Subcommands: []*cli.Command{ + { + Name: "decrypt", + Usage: "Decrypt secrets batch for selected scope and environment", + Aliases: []string{"d"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["secretManager"]), + Flags: flags["secretManager"], + Category: "manager", + BashComplete: system.ShellCompleteCustomOutput, + Action: secretMgrEncryptDecryptAction(conf), + }, + { + Name: "encrypt", + Usage: "Encrypt secrets batch for selected scope and environment", + Aliases: []string{"e"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["secretManager"]), + Flags: flags["secretManager"], + Category: "manager", + BashComplete: system.ShellCompleteCustomOutput, + Action: secretMgrEncryptDecryptAction(conf), + }, + { + Name: "generate", + Usage: "Generate secrets batch for selected scope and environment", + Aliases: []string{"g"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["secretGenerate"]), + Flags: flags["secretGenerate"], + Category: "manager", + BashComplete: system.ShellCompleteCustomOutput, + Action: secretMgrGenerateAction(conf), + }, + }, + }, + { + Name: "keys", + Usage: "SOPS age keys management", + Aliases: []string{"k"}, + Category: "secret", + Subcommands: []*cli.Command{ + { + Name: "create", + Usage: "Create SOPS age keys", + Aliases: []string{"c"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + Category: "keys", + BashComplete: system.ShellCompleteCustomOutput, + Action: secretKeysCreateAction(conf), + }, + { + Name: "download", + Usage: "Download SOPS age keys from S3 bucket", + Aliases: []string{"d"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + Category: "keys", + BashComplete: system.ShellCompleteCustomOutput, + Action: secretKeysDownloadAction(conf), + }, + { + Name: "upload", + Usage: "Upload SOPS age keys to S3 bucket", + Aliases: []string{"u"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + Category: "keys", + BashComplete: system.ShellCompleteCustomOutput, + Action: secretKeysUploadAction(conf), + }, + }, + }, + { + Name: "encrypt", + Usage: "Encrypt secret file", + Aliases: []string{"e"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + Category: "secret", + BashComplete: system.ShellCompleteCustomOutput, + Action: secretAction(conf, SecretRunner.helmSecretsEncrypt), + }, + { + Name: "decrypt", + Usage: "Decrypt secret file", + Aliases: []string{"d"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + Category: "secret", + BashComplete: system.ShellCompleteCustomOutput, + Action: secretAction(conf, SecretRunner.helmSecretsDecrypt), + }, + { + Name: "view", + Usage: "View secret file", + Aliases: []string{"v"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + Category: "secret", + BashComplete: system.ShellCompleteCustomOutput, + Action: secretAction(conf, SecretRunner.helmSecretsView), + }, + { + Name: "edit", + Usage: "Edit secret file", + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + Category: "secret", + BashComplete: system.ShellCompleteCustomOutput, + Action: secretAction(conf, SecretRunner.helmSecretsEdit), + }, + }, + }, + { + Name: "update", + Usage: "Update RMK CLI to a new version", + Flags: flags["update"], + BashComplete: system.ShellCompleteCustomOutput, + Action: updateAction(), + }, + } +} + +func initInputSourceWithContext(gitSpec *git_handler.GitSpec, flags []cli.Flag) cli.BeforeFunc { + return func(ctx *cli.Context) error { + if err := gitSpec.GenerateID(); err != nil { + return err + } + + return inputSourceContext(ctx, flags, system.GetHomePath(system.RMKDir, system.RMKConfig, gitSpec.ID+".yaml")) + } +} + +func readInputSourceWithContext(gitSpec *git_handler.GitSpec, conf *config.Config, flags []cli.Flag) cli.BeforeFunc { + return func(ctx *cli.Context) error { + if err := gitSpec.GenerateID(); err != nil { + return err + } + + configPath := system.GetHomePath(system.RMKDir, system.RMKConfig, gitSpec.ID+".yaml") + if err := conf.ReadConfigFile(configPath); err != nil { + zap.S().Errorf(system.ConfigNotInitializedErrorText) + return err + } + + return inputSourceContext(ctx, flags, configPath) + } +} + +func inputSourceContext(ctx *cli.Context, flags []cli.Flag, configPath string) error { + createInputSource := func(ctx *cli.Context) (altsrc.InputSourceContext, error) { + if ctx.IsSet("config") { + filePath := ctx.String("config") + return altsrc.NewYamlSourceFromFile(filePath) + } + + if err := ctx.Set("config", configPath); err != nil { + return nil, err + } + + if system.IsExists(configPath, true) { + return altsrc.NewYamlSourceFromFile(configPath) + } else { + return &altsrc.MapInputSource{}, nil + } + } + + inputSource, err := createInputSource(ctx) + if err != nil { + return err + } + + return altsrc.ApplyInputSourceValues(ctx, inputSource, flags) +} diff --git a/commands/config_category.go b/commands/config_category.go new file mode 100644 index 0000000..fd6b22c --- /dev/null +++ b/commands/config_category.go @@ -0,0 +1,602 @@ +package commands + +import ( + "encoding/json" + "fmt" + "os" + "strconv" + "strings" + "time" + + "github.com/Masterminds/semver" + "github.com/urfave/cli/v2" + "go.uber.org/zap" + + "rmk/config" + "rmk/git_handler" + "rmk/system" +) + +type ConfigCommands struct { + *ReleaseCommands +} + +func newConfigCommands(conf *config.Config, ctx *cli.Context, workDir string) *ConfigCommands { + return &ConfigCommands{&ReleaseCommands{Conf: conf, Ctx: ctx, WorkDir: workDir}} +} + +func (c *ConfigCommands) awsConfigure(profile string) *system.SpecCMD { + return &system.SpecCMD{ + Args: []string{"configure", "--profile", profile}, + Envs: []string{ + "AWS_CONFIG_FILE=" + strings.Join(c.Conf.AWSSharedConfigFile(profile), ""), + "AWS_SHARED_CREDENTIALS_FILE=" + strings.Join(c.Conf.AWSSharedCredentialsFile(profile), ""), + }, + Command: "aws", + Ctx: c.Ctx.Context, + Dir: c.WorkDir, + Debug: false, + } +} + +func (c *ConfigCommands) helmPlugin() *system.SpecCMD { + return &system.SpecCMD{ + Args: []string{"plugin"}, + Command: "helm", + Dir: c.WorkDir, + Ctx: c.Ctx.Context, + DisableStdOut: true, + Debug: false, + } +} + +func (c *ConfigCommands) rmkConfigInit() *system.SpecCMD { + exRMK, err := os.Executable() + if err != nil { + panic(err) + } + + return &system.SpecCMD{ + Args: []string{"config", "init"}, + Command: exRMK, + Dir: c.WorkDir, + Ctx: c.Ctx.Context, + Debug: true, + } +} + +func (c *ConfigCommands) checkAwsEnv() (map[string]string, bool) { + awsEnvs := map[string]string{ + "region": "AWS_REGION", + "aws_access_key_id": "AWS_ACCESS_KEY_ID", + "aws_secret_access_key": "AWS_SECRET_ACCESS_KEY", + "aws_session_token": "AWS_SESSION_TOKEN", + } + + for key, val := range awsEnvs { + value, ok := os.LookupEnv(val) + if !ok { + delete(awsEnvs, key) + } else { + awsEnvs[key] = value + } + } + + if len(awsEnvs) > 0 { + return awsEnvs, true + } else { + return nil, false + } +} + +func (c *ConfigCommands) configAws() error { + if awsEnvs, ok := c.checkAwsEnv(); !ok { + c.SpecCMD = c.awsConfigure(c.Conf.Profile) + return runner(c).runCMD() + } else { + for key, val := range awsEnvs { + c.SpecCMD = c.awsConfigure(c.Conf.Profile) + c.SpecCMD.Args = append(c.SpecCMD.Args, "set", key, val) + if err := runner(c).runCMD(); err != nil { + return err + } + } + + zap.S().Infof("AWS profile by name %s was created", c.Conf.Profile) + return nil + } +} + +func (c *ConfigCommands) configAwsMFA() error { + var tokenExpiration time.Time + currentTime := time.Now() + regularProfile := c.Conf.Profile + + if len(c.Conf.AWSMFATokenExpiration) > 0 { + unixTime, err := strconv.ParseInt(c.Conf.AWSMFATokenExpiration, 10, 64) + if err != nil { + return err + } + + tokenExpiration = time.Unix(unixTime, 0) + } + + if len(c.Conf.AWSMFAProfile) > 0 { + c.Conf.AwsConfigure.Profile = c.Conf.AWSMFAProfile + } + + if err := c.Conf.GetMFADevicesSerialNumbers(); err != nil { + return err + } + + timeDiff := time.Time{}.Add(tokenExpiration.Sub(currentTime)).Format("15:04:05") + + if len(c.Conf.MFADeviceSerialNumber) > 0 { + zap.S().Infof("MFA device SerialNumber: %s", c.Conf.MFADeviceSerialNumber) + } + + if currentTime.Before(tokenExpiration) { + zap.S().Infof("MFA remaining time for token validity: %s", timeDiff) + } + + if len(c.Conf.MFADeviceSerialNumber) > 0 && currentTime.After(tokenExpiration) { + if err := c.Conf.GetMFASessionToken(); err != nil { + return err + } + + c.Conf.AWSMFAProfile = regularProfile + "-mfa" + c.Conf.AWSMFATokenExpiration = strconv.FormatInt(c.Conf.Expiration.Unix(), 10) + + MFAProfileArgs := map[string]string{ + "aws_access_key_id": c.Conf.MFAProfileCredentials.AccessKeyID, + "aws_secret_access_key": c.Conf.MFAProfileCredentials.SecretAccessKey, + "output": "text", + "region": c.Conf.Region, + } + + regularProfileArgs := map[string]string{ + "aws_access_key_id": c.Conf.MFAToken.AccessKeyId, + "aws_secret_access_key": c.Conf.MFAToken.SecretAccessKey, + "aws_session_token": c.Conf.MFAToken.SessionToken, + } + + for key, val := range MFAProfileArgs { + c.SpecCMD = c.awsConfigure(c.Conf.AWSMFAProfile) + c.SpecCMD.Args = append(c.SpecCMD.Args, "set", key, val) + if err := runner(c).runCMD(); err != nil { + return err + } + } + + for key, val := range regularProfileArgs { + c.SpecCMD = c.awsConfigure(regularProfile) + c.SpecCMD.Args = append(c.SpecCMD.Args, "set", key, val) + if err := runner(c).runCMD(); err != nil { + return err + } + } + } + + c.Conf.AwsConfigure.Profile = regularProfile + + return nil +} + +func (c *ConfigCommands) copyAWSProfile(profile string) error { + profileArgs := map[string]string{ + "aws_access_key_id": c.Conf.MFAProfileCredentials.AccessKeyID, + "aws_secret_access_key": c.Conf.MFAProfileCredentials.SecretAccessKey, + "output": "text", + "region": c.Conf.Region, + } + + for key, val := range profileArgs { + c.SpecCMD = c.awsConfigure(profile) + c.SpecCMD.Args = append(c.SpecCMD.Args, "set", key, val) + if err := runner(c).runCMD(); err != nil { + return err + } + } + + return nil +} + +func (c *ConfigCommands) uninstallHelmPlugin(plugin config.Package) error { + c.SpecCMD = c.helmPlugin() + c.SpecCMD.Args = append(c.SpecCMD.Args, "list") + plSemVer, _ := semver.NewVersion(plugin.Version) + + if err := runner(c).runCMD(); err != nil { + return fmt.Errorf("get Helm plugin list failed: %s", c.SpecCMD.StderrBuf.String()) + } + + for _, v := range strings.Split(c.SpecCMD.StdoutBuf.String(), "\n") { + if strings.Contains(v, plugin.Name) && !strings.Contains(v, plSemVer.String()) { + zap.S().Infof("Helm plugin %s detect new version %s from config", plugin.Name, plugin.Version) + c.SpecCMD = c.helmPlugin() + c.SpecCMD.Args = append(c.SpecCMD.Args, "uninstall", plugin.Name) + if err := runner(c).runCMD(); err != nil { + return fmt.Errorf("Helm plugin %s uninstallation failed: \n%s", + plugin.Name, c.SpecCMD.StderrBuf.String()) + } + + break + } + } + + return nil +} + +func (c *ConfigCommands) installHelmPlugin(plugin config.Package, args ...string) error { + c.SpecCMD = c.helmPlugin() + c.SpecCMD.Args = append(c.SpecCMD.Args, args...) + if err := runner(c).runCMD(); err != nil { + if !strings.Contains(c.SpecCMD.StderrBuf.String(), system.HelmPluginExist) { + return fmt.Errorf("Helm plugin %s installation failed: \n%s", plugin.Name, c.SpecCMD.StderrBuf.String()) + } + } + + if !strings.Contains(c.SpecCMD.StderrBuf.String(), system.HelmPluginExist) { + zap.S().Infof("installing Helm plugin: %s", plugin.Name) + } + + return nil +} + +func (c *ConfigCommands) configHelmPlugins() error { + for _, plugin := range c.Conf.HelmPlugins { + if err := c.uninstallHelmPlugin(*plugin); err != nil { + return err + } + + if err := c.installHelmPlugin(*plugin, "install", plugin.Url, "--version="+plugin.Version); err != nil { + return err + } + } + + return nil +} + +func (c *ConfigCommands) rmkConfig() error { + c.SpecCMD = c.rmkConfigInit() + return runner(c).runCMD() +} + +func initAWSProfile(c *cli.Context, conf *config.Config, gitSpec *git_handler.GitSpec) error { + var profile string + + // Detect if MFA is enabled + if len(conf.AWSMFAProfile) > 0 && len(conf.AWSMFATokenExpiration) > 0 { + profile = conf.AWSMFAProfile + } else { + profile = conf.Profile + } + + if c.Bool("aws-reconfigure") { + if err := os.RemoveAll(strings.Join(conf.AWSSharedCredentialsFile(conf.Profile), "")); err != nil { + return err + } + + // Reconfigure regular AWS profile + if err := newConfigCommands(conf, c, system.GetPwdPath("")).configAws(); err != nil { + return err + } + + // Get CallerIdentity and region for regular AWS profile + if _, err := conf.AwsConfigure.GetAwsConfigure(conf.Profile); err != nil { + return err + } + + // Delete MFA profile + if strings.Contains(profile, "-mfa") { + if err := os.RemoveAll(strings.Join(conf.AWSSharedConfigFile(profile), "")); err != nil { + return err + } + + if err := os.RemoveAll(strings.Join(conf.AWSSharedCredentialsFile(profile), "")); err != nil { + return err + } + } + + // Reset ConfigFrom value for config for current environment + conf.ConfigFrom = gitSpec.ID + // Reset AWSMFAProfile value for config for current environment + conf.AWSMFAProfile = "" + // Reset AWSMFATokenExpiration value for config for current environment + conf.AWSMFATokenExpiration = "" + // Returning a regular profile value + profile = conf.Profile + + // Create new MFA profile + if err := newConfigCommands(conf, c, system.GetPwdPath("")).configAwsMFA(); err != nil { + return err + } + } + + if ok, err := conf.AwsConfigure.GetAwsConfigure(profile); err != nil && ok { + zap.S().Warnf("%s", err.Error()) + if err := newConfigCommands(conf, c, system.GetPwdPath("")).configAws(); err != nil { + return err + } + + if _, err := conf.AwsConfigure.GetAwsConfigure(profile); err != nil { + return err + } + + if err := newConfigCommands(conf, c, system.GetPwdPath("")).configAwsMFA(); err != nil { + return err + } + } else if !c.Bool("aws-reconfigure") { + if err := newConfigCommands(conf, c, system.GetPwdPath("")).configAwsMFA(); err != nil { + return err + } + } else if !ok && err != nil { + return err + } + + return nil +} + +func getConfigFromEnvironment(c *cli.Context, conf *config.Config, gitSpec *git_handler.GitSpec) error { + if len(c.String("config-from-environment")) > 0 { + configPath := system.GetHomePath(system.RMKDir, system.RMKConfig, + gitSpec.RepoPrefixName+"-"+c.String("config-from-environment")+".yaml") + + if err := conf.ReadConfigFile(configPath); err != nil { + zap.S().Errorf("RMK config %s.yaml not initialized, please checkout to branch %s "+ + "and run command 'rmk config init' with specific parameters", + c.String("config-from-environment"), c.String("config-from-environment")) + return err + } + + if err := c.Set("config-from", conf.Name); err != nil { + return err + } + + // Delete regular profile + if err := os.RemoveAll(strings.Join(conf.AWSSharedCredentialsFile(gitSpec.ID), "")); err != nil { + return err + } + + if len(conf.AWSMFAProfile) > 0 && len(conf.AWSMFATokenExpiration) > 0 { + regularProfile := conf.Profile + + // Get MFA profile credentials. + conf.AwsConfigure.Profile = conf.AWSMFAProfile + if err := conf.GetAWSCredentials(); err != nil { + return err + } + + // Copy MFA profile for current environment + conf.AwsConfigure.Profile = regularProfile + if err := newConfigCommands(conf, c, system.GetPwdPath("")).copyAWSProfile(gitSpec.ID); err != nil { + return err + } + } else { + // Delete config MFA profile + if err := os.RemoveAll(strings.Join(conf.AWSSharedConfigFile(gitSpec.ID+"-mfa"), "")); err != nil { + return err + } + + // Delete credentials MFA profile + if err := os.RemoveAll(strings.Join(conf.AWSSharedCredentialsFile(gitSpec.ID+"-mfa"), "")); err != nil { + return err + } + + // Get regular profile credentials. + if err := conf.GetAWSCredentials(); err != nil { + return err + } + + // Copy regular profile for current environment + if err := newConfigCommands(conf, c, system.GetPwdPath("")).copyAWSProfile(gitSpec.ID); err != nil { + return err + } + } + + // Reset AWSMFAProfile value for config for current environment + if err := c.Set("aws-mfa-profile", ""); err != nil { + return err + } + + // Reset AWSMFATokenExpiration value for config for current environment + if err := c.Set("aws-mfa-token-expiration", ""); err != nil { + return err + } + + conf.ConfigFrom = c.String("config-from") + conf.AwsConfigure.Profile = gitSpec.ID + + return nil + } + + if err := system.ValidateArtifactModeDefault(c, "required parameter --github-token not set"); err != nil { + return err + } + + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + if !c.IsSet("config-from") { + if err := c.Set("config-from", gitSpec.ID); err != nil { + return err + } + } + + conf.ConfigFrom = c.String("config-from") + conf.AwsConfigure.Profile = gitSpec.ID + conf.CloudflareToken = c.String("cloudflare-token") + conf.GitHubToken = c.String("github-token") + + return nil +} + +func configDeleteAction(conf *config.Config) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + // Delete MFA profile + if len(conf.AWSMFAProfile) > 0 && len(conf.AWSMFATokenExpiration) > 0 { + if err := os.RemoveAll(strings.Join(conf.AWSSharedConfigFile(conf.AWSMFAProfile), "")); err != nil { + return err + } + + if err := os.RemoveAll(strings.Join(conf.AWSSharedCredentialsFile(conf.AWSMFAProfile), "")); err != nil { + return err + } + } + + // Delete config MFA profile + if err := os.RemoveAll(strings.Join(conf.AWSSharedConfigFile(conf.Profile), "")); err != nil { + return err + } + + // Delete credentials MFA profile + if err := os.RemoveAll(strings.Join(conf.AWSSharedCredentialsFile(conf.Profile), "")); err != nil { + return err + } + + if err := os.RemoveAll(c.String("config")); err != nil { + return err + } + + zap.S().Infof("deleted config file by path: %s", c.String("config")) + + return nil + } +} + +func configInitAction(conf *config.Config, gitSpec *git_handler.GitSpec) cli.ActionFunc { + return func(c *cli.Context) error { + if err := getConfigFromEnvironment(c, conf, gitSpec); err != nil { + return err + } + + zap.S().Infof("loaded config file by path: %s", c.String("config")) + + start := time.Now() + + conf.Name = gitSpec.ID + conf.Tenant = gitSpec.RepoPrefixName + conf.Environment = gitSpec.DefaultBranch + zap.S().Infof("RMK will use values for %s environment", conf.Environment) + + if c.Bool("slack-notifications") { + conf.SlackNotifications = c.Bool("slack-notifications") + if !c.IsSet("slack-webhook") || !c.IsSet("slack-channel") { + return fmt.Errorf("parameters --slack-webhook, --slack-channel not set, " + + "required if Slack notifications are enabled") + } else { + conf.SlackWebHook = c.String("slack-webhook") + conf.SlackChannel = c.String("slack-channel") + conf.SlackMsgDetails = c.StringSlice("slack-message-details") + } + } + + conf.ArtifactMode = c.String("artifact-mode") + conf.ProgressBar = c.Bool("progress-bar") + conf.Terraform.BucketKey = system.TenantBucketKey + conf.ClusterProvisionerSL = c.Bool("cluster-provisioner-state-locking") + conf.S3ChartsRepoRegion = c.String("s3-charts-repo-region") + conf.ClusterProvider = c.String("cluster-provider") + conf.AWSMFAProfile = c.String("aws-mfa-profile") + conf.AWSMFATokenExpiration = c.String("aws-mfa-token-expiration") + conf.AWSECRHost = c.String("aws-ecr-host") + conf.AWSECRRegion = c.String("aws-ecr-region") + conf.AWSECRUserName = c.String("aws-ecr-user-name") + + // AWS Profile init configuration with support MFA + if err := initAWSProfile(c, conf, gitSpec); err != nil { + return err + } + + //Formation of a unique bucket name, consisting of the prefix tenant of the repository, + //constant and the first 3 and last 2 numbers AWS account id + awsUID := conf.AccountID[0:3] + conf.AccountID[len(conf.AccountID)-2:] + conf.SopsAgeKeys = system.GetHomePath(system.RMKDir, system.SopsRootName, conf.Tenant+"-"+system.SopsRootName+"-"+awsUID) + conf.SopsBucketName = conf.Tenant + "-" + system.SopsRootName + "-" + awsUID + conf.Terraform.BucketName = conf.Tenant + "-" + system.TenantBucketName + "-" + awsUID + conf.Terraform.DDBTableName = system.TenantDDBTablePrefix + "-" + awsUID + + if err := conf.InitConfig(true).SetRootDomain(c, gitSpec.ID); err != nil { + return err + } + + if err := conf.CreateConfigFile(); err != nil { + return err + } + + if conf.ClusterProvider == system.AWSClusterProvider { + if conf.ClusterProvisionerSL { + // create dynamodb table for backend terraform + if err := conf.CreateDynamoDBTable(conf.Terraform.DDBTableName); err != nil { + return err + } + } + + // create s3 bucket for backend terraform + if err := conf.CreateBucket(conf.Terraform.BucketName); err != nil { + return err + } + + //create s3 bucket for sops age keys + if err := conf.CreateBucket(conf.SopsBucketName); err != nil { + return err + } + + if err := conf.DownloadFromBucket("", conf.SopsBucketName, conf.SopsAgeKeys, conf.Tenant); err != nil { + return err + } + } + + zap.S().Infof("time spent on initialization: %.fs", time.Since(start).Seconds()) + + return nil + } +} + +func configListAction(conf *config.Config, gitSpec *git_handler.GitSpec) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + if err := gitSpec.GenerateID(); err != nil { + return err + } + + conf.Tenant = gitSpec.RepoPrefixName + return conf.GetConfigs(c.Bool("all")) + } +} + +func configViewAction(conf *config.Config) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + if c.String("log-format") == "json" { + serializeJsonConfig, err := conf.SerializeJsonConfig() + if err != nil { + return err + } + + zap.L().Info("RMK", zap.Any("config", json.RawMessage(serializeJsonConfig))) + return nil + } + + serializeConfig, err := conf.SerializeConfig() + if err != nil { + return err + } + + zap.S().Infof("loaded config file by path: %s", c.String("config")) + fmt.Printf("%s\n", string(serializeConfig)) + + return nil + } +} diff --git a/commands/container_registry_category.go b/commands/container_registry_category.go new file mode 100644 index 0000000..a73a260 --- /dev/null +++ b/commands/container_registry_category.go @@ -0,0 +1,91 @@ +package commands + +import ( + "fmt" + "strings" + + "github.com/urfave/cli/v2" + "go.uber.org/zap" + + "rmk/config" + "rmk/system" +) + +type DockerRunner interface { + dockerLogin() error + dockerLogout() error +} + +type CRCommands struct { + *ReleaseCommands +} + +func newCRCommands(conf *config.Config, ctx *cli.Context, workDir string) *CRCommands { + return &CRCommands{&ReleaseCommands{Conf: conf, Ctx: ctx, WorkDir: workDir}} +} + +func (cr *CRCommands) docker(args ...string) *system.SpecCMD { + return &system.SpecCMD{ + Args: append([]string{}, args...), + Command: "docker", + Dir: cr.WorkDir, + Ctx: cr.Ctx.Context, + DisableStdOut: true, + Debug: false, + } +} + +func (cr *CRCommands) dockerLogin() error { + credentials, err := cr.Conf.AwsConfigure.GetECRCredentials(cr.Conf.AWSECRRegion) + if err != nil { + return err + } + + if token, ok := credentials[cr.Conf.AWSECRUserName]; !ok { + return fmt.Errorf("failed to get token") + } else { + if cr.Ctx.Bool("get-token") { + fmt.Println(token) + return nil + } + + cr.SpecCMD = cr.docker("login", "--username", cr.Conf.AWSECRUserName, "--password", token, + cr.Conf.AWSECRHost) + if err := runner(cr).runCMD(); err != nil { + return err + } + + if !strings.Contains(cr.SpecCMD.StderrBuf.String(), "Using --password") { + return fmt.Errorf(strings.ReplaceAll(cr.SpecCMD.StderrBuf.String(), "\n", "")) + } + + zap.S().Info(strings.ReplaceAll(cr.SpecCMD.StdoutBuf.String(), "\n", "")) + } + + return nil +} + +func (cr *CRCommands) dockerLogout() error { + cr.SpecCMD = cr.docker("logout", cr.Conf.AWSECRHost) + if err := runner(cr).runCMD(); err != nil { + return err + } + + zap.S().Info(strings.ReplaceAll(cr.SpecCMD.StdoutBuf.String(), "\n", "")) + + return nil +} + +func containerRegistryAction(conf *config.Config, action func(dockerRunner DockerRunner) error) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateArtifactModeDefault(c, ""); err != nil { + return err + } + + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + return action(newCRCommands(conf, c, system.GetPwdPath(""))) + } +} diff --git a/commands/download_packages.go b/commands/download_packages.go new file mode 100644 index 0000000..66fc9c3 --- /dev/null +++ b/commands/download_packages.go @@ -0,0 +1,853 @@ +package commands + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "strings" + "syscall" + "time" + + "github.com/Masterminds/semver" + "github.com/urfave/cli/v2" + "go.uber.org/zap" + + "rmk/aws_provider" + "rmk/config" + "rmk/go_getter" + "rmk/system" +) + +const ( + gitProvider = "git" + httpProvider = "http" +) + +var ( + TenantPrDependenciesDir = filepath.Join(system.TenantProjectDIR, "dependencies") + TenantPrInventoryDir = filepath.Join(system.TenantProjectDIR, "inventory") + TenantPrInvClustersDir = filepath.Join(TenantPrInventoryDir, "clusters") + TenantPrInvHooksDir = filepath.Join(TenantPrInventoryDir, "hooks") +) + +type SpecDownload struct { + Conf *config.Config + Ctx *cli.Context + PkgUrl string + PkgDst string + PkgFile string + PkgName string + Header *http.Header + Type string + Artifact *ArtifactSpec + rmOldDir bool +} + +type ArtifactSpec struct { + BucketName string + Key string + Region string + Version string + Url string +} + +type RMKArtifactMetadata struct { + ProjectName string `json:"project_name"` + Tag string `json:"tag"` + PreviousTag string `json:"previous_tag"` + Version string `json:"version"` + Commit string `json:"commit"` + Date time.Time `json:"date"` + Runtime struct { + Goos string `json:"goos"` + Goarch string `json:"goarch"` + } `json:"runtime"` +} + +type InventoryState struct { + clustersState map[string]struct{} + helmPluginsState map[string]struct{} + toolsState map[string]struct{} +} + +func (s *SpecDownload) parseUrlByType() error { + var getter = "git::" + s.Header = &http.Header{} + if strings.HasPrefix(s.PkgUrl, getter) { + s.Type = gitProvider + } else { + s.Type = httpProvider + } + + if s.Type == gitProvider { + parse, err := url.Parse(strings.TrimPrefix(s.PkgUrl, getter)) + if err != nil { + return err + } + + if parse.Query().Has("depth") { + return fmt.Errorf("for Git provider in project file %s repository only 'ref' argument can be used", s.PkgName) + } + + query := parse.Query() + query.Add("depth", "1") + s.PkgUrl = getter + parse.Scheme + "://user:" + s.Conf.GitHubToken + "@" + parse.Host + parse.Path + "?" + query.Encode() + } + + return nil +} + +func (s *SpecDownload) downloadErrorHandler(err error) error { + switch { + case err != nil && s.Type == gitProvider: + return fmt.Errorf("failed to download %s object %s, potential reasons: "+ + "object not found, permission denied, credentials expired, URL format in project file not correct", s.Type, s.PkgName) + case err != nil && s.Type == httpProvider: + return fmt.Errorf("failed to download %s object %s, potential reasons: "+ + "object not found, URL format in project file not correct", s.Type, s.PkgName) + } + + return err +} + +func (s *SpecDownload) download(silent bool) error { + if err := s.parseUrlByType(); err != nil { + return err + } + + if !silent { + zap.S().Infof("starting download package: %s", s.PkgName) + } + + return s.downloadErrorHandler( + go_getter.DownloadArtifact(s.PkgUrl, s.PkgDst, s.PkgName, s.Header, silent, s.Conf.ProgressBar, s.Ctx.Context), + ) +} + +func (s *SpecDownload) parseArtifactUrl() error { + u, err := url.Parse(s.Artifact.Url) + if err != nil { + return err + } + + // This just check whether we are dealing with S3 or + // any other S3 compliant service. S3 has a predictable + // url as others do not + if strings.Contains(u.Host, "amazonaws.com") { + // Amazon S3 supports both virtual-hosted–style and path-style URLs to access a bucket, although path-style is deprecated + // In both cases few older regions supports dash-style region indication (s3-Region) even if AWS discourages their use. + // The same bucket could be reached with: + // bucket.s3.region.amazonaws.com/path + // bucket.s3-region.amazonaws.com/path + // s3.amazonaws.com/bucket/path + // s3-region.amazonaws.com/bucket/path + + hostParts := strings.Split(u.Host, ".") + switch len(hostParts) { + // path-style + case 3: + // Parse the region out of the first part of the host + s.Artifact.Region = strings.TrimPrefix(strings.TrimPrefix(hostParts[0], "s3-"), "s3") + if s.Artifact.Region == "" { + s.Artifact.Region = "us-east-1" + } + + pathParts := strings.SplitN(u.Path, "/", 3) + s.Artifact.BucketName = pathParts[1] + s.Artifact.Key = pathParts[2] + // vhost-style, dash region indication + case 4: + // Parse the region out of the first part of the host + s.Artifact.Region = strings.TrimPrefix(strings.TrimPrefix(hostParts[1], "s3-"), "s3") + if s.Artifact.Region == "" { + return fmt.Errorf("artifact URL is not valid S3 URL for dependency name: %s", s.PkgName) + } + + pathParts := strings.SplitN(u.Path, "/", 2) + s.Artifact.BucketName = hostParts[0] + s.Artifact.Key = pathParts[1] + //vhost-style, dot region indication + case 5: + s.Artifact.Region = hostParts[2] + pathParts := strings.SplitN(u.Path, "/", 2) + s.Artifact.BucketName = hostParts[0] + s.Artifact.Key = pathParts[1] + + } + + if len(hostParts) < 3 && len(hostParts) > 5 { + return fmt.Errorf("artifact URL is not valid S3 URL for dependency name: %s", s.PkgName) + } + + if len(strings.SplitN(s.Artifact.Key, "/", 2)) < 1 { + return fmt.Errorf("artifact URL is not valid S3 URL for dependency name %s, path does not contain SemVer2", s.PkgName) + } + + s.Artifact.Version = strings.SplitN(s.Artifact.Key, "/", 2)[0] + + _, err := semver.NewVersion(s.Artifact.Version) + if err != nil { + return fmt.Errorf("%s for dependency name: %s", err.Error(), s.PkgName) + } + + return nil + } else { + return fmt.Errorf("artifact URL is not valid S3 compliant URL for dependency name: %s", s.PkgName) + } +} + +func (s *SpecDownload) updateArtifact() error { + currentProfile := s.Conf.Profile + licenseProfile := s.Conf.Profile + "-license" + + s.Conf.AwsConfigure.Profile = licenseProfile + if ok, err := s.Conf.GetAwsConfigure(licenseProfile); err != nil && ok { + zap.S().Warnf("%s", err.Error()) + if err := newConfigCommands(s.Conf, s.Ctx, system.GetPwdPath("")).configAws(); err != nil { + return err + } + + if _, err := s.Conf.GetAwsConfigure(licenseProfile); err != nil { + return err + } + } else if s.Ctx.Bool("aws-reconfigure-artifact-license") { + if err := newConfigCommands(s.Conf, s.Ctx, system.GetPwdPath("")).configAws(); err != nil { + return err + } + } + + if err := s.parseArtifactUrl(); err != nil { + return err + } + + if artVerExist, err := s.Conf.BucketKeyExists(s.Artifact.Region, s.Artifact.BucketName, s.Artifact.Key); err != nil || !artVerExist { + return err + } + + if err := s.Conf.DownloadFromBucket(s.Artifact.Region, s.Artifact.BucketName, system.GetPwdPath(system.ArtifactDownloadDir), s.Artifact.Key); err != nil { + return err + } + + s.Conf.AwsConfigure.Profile = currentProfile + if system.IsExists(s.PkgDst, false) { + if err := os.MkdirAll(s.PkgDst, 0777); err != nil { + return err + } + } + + r, err := os.Open(filepath.Join(system.GetPwdPath(system.ArtifactDownloadDir), s.Artifact.Key)) + if err != nil { + return err + } + + if err := system.UnTar(s.PkgDst, "", r); err != nil { + return err + } + + if system.IsExists(filepath.Join(s.PkgDst, system.TenantProjectDIR, "inventory"), false) { + if err := system.CopyDir(filepath.Join(s.PkgDst, system.TenantProjectDIR, "inventory"), system.GetPwdPath(system.TenantProjectDIR)); err != nil { + return err + } + } + + if err := os.RemoveAll(system.GetPwdPath(system.ArtifactDownloadDir)); err != nil { + return err + } + + return nil +} + +// depsGC - deleting old deps dirs with not actual versions +func hooksGC(hooks []config.HookMapping) error { + if !system.IsExists(system.GetPwdPath(TenantPrInvHooksDir), false) { + return nil + } + + allDirs, _, err := system.ListDir(system.GetPwdPath(TenantPrInvHooksDir), true) + if err != nil { + return err + } + + var diff = make(map[string]struct{}, len(allDirs)) + for _, val := range allDirs { + diff[val] = struct{}{} + } + + for _, hook := range hooks { + if _, ok := diff[hook.DstPath]; ok { + delete(diff, hook.DstPath) + } + } + + for dir := range diff { + if err := os.RemoveAll(dir); err != nil { + return err + } + } + + return nil +} + +// resolveHooks - resolve hooks version according to nested project.yaml file +func resolveHooks(hooks map[string]*config.Package, tenant string, conf *config.Config) error { + if len(hooks) > 0 { + for _, hook := range hooks { + conf.HooksMapping = append(conf.HooksMapping, + config.HookMapping{ + Tenant: tenant, + Exists: true, + Package: hook, + }, + ) + } + } else if len(conf.Dependencies) > 0 { + conf.HooksMapping = append(conf.HooksMapping, + config.HookMapping{ + Tenant: tenant, + Exists: false, + Package: &config.Package{}, + }, + ) + } + + return nil +} + +// uniqueHooksMapping - casts a list of hooks to unique values and recursively inherits hook version values +func uniqueHooksMapping(hooks []config.HookMapping) []config.HookMapping { + var uniqueHooksMapping []config.HookMapping + + for _, hook := range hooks { + skip := false + for _, uniqueHook := range uniqueHooksMapping { + if hook.Exists == uniqueHook.Exists && hook.Tenant == uniqueHook.Tenant { + skip = true + break + } + } + + if !skip { + uniqueHooksMapping = append(uniqueHooksMapping, hook) + } + } + + numberHook := 0 + compareHooks := make(map[int]*semver.Version) + for key, hook := range uniqueHooksMapping { + if hook.Exists { + ver, _ := semver.NewVersion(hook.Version) + compareHooks[key] = ver + } + } + + for key, ver := range compareHooks { + if len(compareHooks) > 1 { + for _, v := range compareHooks { + if ver.GreaterThan(v) { + numberHook = key + } + } + } else { + numberHook = key + } + } + + for key, hook := range uniqueHooksMapping { + if !hook.Exists { + uniqueHooksMapping[key].Package = uniqueHooksMapping[numberHook].Package + uniqueHooksMapping[key].InheritedFrom = uniqueHooksMapping[numberHook].Tenant + } + } + + return uniqueHooksMapping +} + +func (is *InventoryState) saveState(inv config.Inventory) { + is.clustersState = make(map[string]struct{}) + for key := range inv.Clusters { + is.clustersState[key] = struct{}{} + } + + is.helmPluginsState = make(map[string]struct{}) + for key := range inv.HelmPlugins { + is.helmPluginsState[key] = struct{}{} + } + + is.toolsState = make(map[string]struct{}) + for key := range inv.Tools { + is.toolsState[key] = struct{}{} + } +} + +func (is *InventoryState) resolveClusters(invPkg map[string]*config.Package, conf *config.Config) (map[string]*config.Package, error) { + if len(conf.Clusters) == 0 { + conf.Clusters = make(map[string]*config.Package) + } + + for key, pkg := range invPkg { + vPkg, _ := semver.NewVersion(pkg.Version) + if _, ok := conf.Clusters[key]; !ok { + conf.Clusters[key] = pkg + } else if _, found := is.clustersState[key]; !found { + vP, _ := semver.NewVersion(conf.Clusters[key].Version) + if vPkg.GreaterThan(vP) { + conf.Clusters[key] = pkg + } + } + } + + return conf.Clusters, nil +} + +func (is *InventoryState) resolveHelmPlugins(invPkg map[string]*config.Package, conf *config.Config) (map[string]*config.Package, error) { + if len(conf.HelmPlugins) == 0 { + conf.HelmPlugins = make(map[string]*config.Package) + } + + for key, pkg := range invPkg { + vPkg, _ := semver.NewVersion(pkg.Version) + if _, ok := conf.HelmPlugins[key]; !ok { + conf.HelmPlugins[key] = pkg + } else if _, found := is.helmPluginsState[key]; !found { + vP, _ := semver.NewVersion(conf.HelmPlugins[key].Version) + if vPkg.GreaterThan(vP) { + conf.HelmPlugins[key] = pkg + } + } + } + + return conf.HelmPlugins, nil +} + +func (is *InventoryState) resolveTools(invPkg map[string]*config.Package, conf *config.Config) (map[string]*config.Package, error) { + if len(conf.Tools) == 0 { + conf.Tools = make(map[string]*config.Package) + } + + for key, pkg := range invPkg { + vPkg, _ := semver.NewVersion(pkg.Version) + if _, ok := conf.Tools[key]; !ok { + conf.Tools[key] = pkg + } else if _, found := is.toolsState[key]; !found { + vP, _ := semver.NewVersion(conf.Tools[key].Version) + if vPkg.GreaterThan(vP) { + conf.Tools[key] = pkg + } + } + } + + return conf.Tools, nil +} + +func resolveDependencies(conf *config.Config, ctx *cli.Context, silent bool) error { + var ( + recursivelyDownload func() error + invErr error + ) + + if err := updateDependencies(conf, ctx, silent); err != nil { + return err + } + + if err := resolveHooks(conf.Hooks, conf.Tenant, conf); err != nil { + return err + } + + invState := &InventoryState{} + invState.saveState(conf.Inventory) + + recursivelyDownload = func() error { + for _, val := range conf.Dependencies { + projectFile := &config.ProjectFile{} + + depsDir := system.FindDir(system.GetPwdPath(TenantPrDependenciesDir), val.Name) + if err := projectFile.ReadProjectFile(system.GetPwdPath(TenantPrDependenciesDir, depsDir, system.TenantProjectFile)); err != nil { + return err + } + + // Resolve and recursively download repositories containing clusters + if conf.Clusters, invErr = invState.resolveClusters(projectFile.Clusters, conf); invErr != nil { + return invErr + } + + // Resolve and recursively download repositories containing helm plugins + if conf.HelmPlugins, invErr = invState.resolveHelmPlugins(projectFile.HelmPlugins, conf); invErr != nil { + return invErr + } + + // Resolve repositories containing hooks + if len(strings.Split(depsDir, ".")) > 0 { + if err := resolveHooks(projectFile.Hooks, strings.Split(depsDir, ".")[0], conf); err != nil { + return err + } + } + + // Resolve and recursively download repositories containing tools + if conf.Tools, invErr = invState.resolveTools(projectFile.Tools, conf); invErr != nil { + return invErr + } + + // Recursively downloading repositories containing helmfiles + foundDeps := 0 + compare := make(map[string]struct{}, len(projectFile.Dependencies)) + for _, dep := range projectFile.Dependencies { + compare[dep.Name] = struct{}{} + } + + for _, dep := range conf.Dependencies { + if _, ok := compare[dep.Name]; ok { + foundDeps++ + } + } + + if len(projectFile.Dependencies) == 0 { + foundDeps++ + } + + if foundDeps == 0 { + conf.Dependencies = append(projectFile.Dependencies, val) + if err := updateDependencies(conf, ctx, silent); err != nil { + return err + } + + if err := recursivelyDownload(); err != nil { + return err + } + } + } + + return nil + } + + if err := recursivelyDownload(); err != nil { + return err + } + + if err := updateClusters(conf, ctx, silent); err != nil { + return err + } + + // Finding unique versions of hooks in HooksMapping + conf.HooksMapping = uniqueHooksMapping(conf.HooksMapping) + + if err := updateHooks(conf, ctx, silent); err != nil { + return err + } + + // Old hooks dirs garbage collection + if err := hooksGC(conf.HooksMapping); err != nil { + return err + } + + if err := updateTools(conf, ctx, silent); err != nil { + return err + } + + if err := newConfigCommands(conf, ctx, system.GetPwdPath("")).configHelmPlugins(); err != nil { + return err + } + + if err := conf.CreateConfigFile(); err != nil { + return err + } + + return nil +} + +func removeOldDir(pwd string, pkg config.Package) error { + if !system.IsExists(pwd, false) { + return nil + } + + oldDir := system.FindDir(pwd, pkg.Name) + if len(strings.Split(oldDir, "-")) > 1 { + oldVer := strings.SplitN(oldDir, "-", 2)[1] + if oldVer != pkg.Version { + if err := os.RemoveAll(filepath.Join(pwd, pkg.Name+"-"+oldVer)); err != nil { + return err + } + } + } + + return nil +} + +func (s *SpecDownload) batchUpdate(pwd string, pkg config.Package, silent bool) error { + s.PkgUrl = pkg.Url + s.PkgName = pkg.Name + "-" + strings.ReplaceAll(pkg.Version, "/", "_") + s.PkgDst = filepath.Join(s.PkgDst, s.PkgName) + pkgExists := system.IsExists(s.PkgDst, false) + if !pkgExists { + if s.rmOldDir && s.Ctx.String("artifact-mode") != system.ArtifactModeOnline { + if err := removeOldDir(pwd, pkg); err != nil { + return err + } + } + + switch { + case s.Ctx.String("artifact-mode") == system.ArtifactModeOnline && len(pkg.ArtifactUrl) > 0: + if err := s.updateArtifact(); err != nil { + return err + } + case s.Ctx.String("artifact-mode") == system.ArtifactModeOnline && len(pkg.ArtifactUrl) == 0: + zap.S().Warnf("overriding %s component in inventory section "+ + "%s file is not allowed when using %s artifact mode", + s.PkgName, system.TenantProjectFile, system.ArtifactModeOnline) + return nil + default: + if err := s.download(silent); err != nil { + return err + } + } + } + + return nil +} + +func updateDependencies(conf *config.Config, ctx *cli.Context, silent bool) error { + pwd := system.GetPwdPath(TenantPrDependenciesDir) + + for key, val := range conf.Dependencies { + spec := &SpecDownload{Conf: conf, Ctx: ctx, PkgDst: pwd, + Artifact: &ArtifactSpec{Url: val.ArtifactUrl}, rmOldDir: true} + if err := spec.batchUpdate(pwd, val, silent); err != nil { + return err + } + + // needed if all packages from project.yaml were downloaded earlier + spec.PkgUrl = val.Url + if err := spec.parseUrlByType(); err != nil { + return err + } + + switch { + case system.IsExists(filepath.Join(spec.PkgDst, system.HelmfileFileName), true): + conf.Dependencies[key].DstPath = filepath.Join(spec.PkgDst, system.HelmfileFileName) + case system.IsExists(filepath.Join(spec.PkgDst, system.HelmfileGoTmplName), true): + conf.Dependencies[key].DstPath = filepath.Join(spec.PkgDst, system.HelmfileGoTmplName) + default: + return fmt.Errorf("%s or %s not found in dependent project %s", + system.HelmfileFileName, system.HelmfileGoTmplName, spec.PkgName) + } + } + + return nil +} + +func updateClusters(conf *config.Config, ctx *cli.Context, silent bool) error { + pwd := system.GetPwdPath(TenantPrInvClustersDir) + + for key, val := range conf.Clusters { + spec := &SpecDownload{Conf: conf, Ctx: ctx, PkgDst: pwd, Artifact: &ArtifactSpec{}, rmOldDir: true} + if err := spec.batchUpdate(pwd, *val, silent); err != nil { + return err + } + + conf.Clusters[key].DstPath = spec.PkgDst + } + + return nil +} + +func updateHooks(conf *config.Config, ctx *cli.Context, silent bool) error { + pwd := system.GetPwdPath(TenantPrInvHooksDir) + + for key, val := range conf.HooksMapping { + spec := &SpecDownload{Conf: conf, Ctx: ctx, PkgDst: pwd, Artifact: &ArtifactSpec{}} + if err := spec.batchUpdate(pwd, *val.Package, silent); err != nil { + return err + } + + conf.HooksMapping[key].DstPath = spec.PkgDst + } + + return nil +} + +func match(dir string, patterns []string) ([]string, error) { + var ( + unique []string + find []string + ) + + for _, val := range patterns { + match, err := system.WalkMatch(dir, val) + if err != nil { + return nil, err + } + + find = append(find, match...) + } + + for _, val := range find { + skip := false + for _, uniq := range unique { + if val == uniq { + skip = true + break + } + } + + if !skip { + unique = append(unique, val) + } + } + + return unique, nil +} + +func overwriteFiles(path, pattern, name string) error { + var data []byte + + oldFilePath, err := system.WalkMatch(path, pattern) + if err != nil { + return err + } + + if err := os.RemoveAll(strings.Join(oldFilePath, "")); err != nil { + return err + } + + return os.WriteFile(filepath.Join(path, name), data, 0755) +} + +func updateTools(conf *config.Config, ctx *cli.Context, silent bool) error { + spec := &SpecDownload{ + Conf: conf, + Ctx: ctx, + PkgDst: system.GetHomePath(system.RMKDir, system.RMKToolsDir, system.ToolsTmpDir), + Type: httpProvider, + } + + toolsVersionPath := system.GetHomePath(system.RMKDir, system.RMKToolsDir, system.ToolsVersionDir) + toolsTmpPath := system.GetHomePath(system.RMKDir, system.RMKToolsDir, system.ToolsTmpDir) + toolsBinPath := system.GetHomePath(".local", system.ToolsBinDir) + + if err := os.MkdirAll(toolsVersionPath, 0755); err != nil { + return err + } + + // Cleaning previously downloaded artifacts state + for _, pkg := range conf.Tools { + pkg.Artifacts = []string{} + } + + for key, val := range conf.Tools { + version, _ := semver.NewVersion(val.Version) + spec.PkgUrl = val.Url + spec.PkgName = val.Name + "-" + version.String() + if !system.IsExists(filepath.Join(toolsVersionPath, spec.PkgName), true) { + err := spec.download(silent) + if err != nil { + return err + } + + if err := overwriteFiles(toolsVersionPath, val.Name+"-*", spec.PkgName); err != nil { + return err + } + + if val.Rename { + conf.Tools[key].Artifacts, err = match(toolsTmpPath, + []string{filepath.Base(val.Url), val.Name + "-*", val.Name + "_*"}) + if err != nil { + return err + } + } else { + conf.Tools[key].Artifacts, err = match(toolsTmpPath, + []string{val.Name, val.Name + "-*", val.Name + "_*"}) + if err != nil { + return err + } + } + } else { + continue + } + } + + if err := os.MkdirAll(toolsBinPath, 0755); err != nil { + return err + } + + for _, pkg := range conf.Tools { + for _, pathArt := range pkg.Artifacts { + if pkg.Rename { + if err := system.CopyFile(pathArt, filepath.Join(toolsBinPath, pkg.Name)); err != nil { + return err + } + } else { + if err := system.CopyFile(pathArt, filepath.Join(toolsBinPath, filepath.Base(pathArt))); err != nil { + return err + } + } + } + } + + return os.RemoveAll(toolsTmpPath) +} + +func getRMKArtifactMetadata(keyPath string) (*RMKArtifactMetadata, error) { + rmkArtifactMetadata := &RMKArtifactMetadata{} + aws := &aws_provider.AwsConfigure{Region: system.RMKBucketRegion} + data, err := aws.GetFileData(system.RMKBucketName, system.RMKBin+"/"+keyPath+"/metadata.json") + if err != nil { + return nil, err + } + + if err := json.Unmarshal(data, &rmkArtifactMetadata); err != nil { + return nil, err + } + + return rmkArtifactMetadata, nil +} + +func rmkURLFormation(paths ...string) string { + u, err := url.Parse("https://" + system.RMKBucketName + ".s3." + system.RMKBucketRegion + ".amazonaws.com") + if err != nil { + zap.S().Fatal(err) + } + + p := append([]string{u.Path}, paths...) + u.Path = path.Join(p...) + return u.String() +} + +func updateRMK(pkgName, version string, silent, progressBar bool, ctx *cli.Context) error { + zap.S().Infof("starting download package: %s", pkgName) + pkgDst := system.GetHomePath(filepath.Join(".local", system.ToolsBinDir)) + if err := go_getter.DownloadArtifact( + rmkURLFormation(system.RMKBin, version, pkgName), + pkgDst, + pkgName, + &http.Header{}, + silent, + progressBar, + ctx.Context, + ); err != nil { + return err + } + + if err := os.Rename(filepath.Join(pkgDst, pkgName), filepath.Join(pkgDst, system.RMKBin)); err != nil { + return err + } + + if err := os.Chmod(filepath.Join(pkgDst, system.RMKBin), 0755); err != nil { + return err + } + + relPath := strings.ReplaceAll(system.RMKSymLinkPath, filepath.Base(system.RMKSymLinkPath), "") + if syscall.Access(relPath, uint32(2)) == nil { + if !system.IsExists(system.RMKSymLinkPath, true) { + return os.Symlink(filepath.Join(pkgDst, system.RMKBin), system.RMKSymLinkPath) + } + } else { + zap.S().Warnf("symlink was not created automatically due to permissions, "+ + "please complete installation by running command: \n"+ + "sudo ln -s %s %s", filepath.Join(pkgDst, system.RMKBin), system.RMKSymLinkPath) + } + + return nil +} diff --git a/commands/flags.go b/commands/flags.go new file mode 100644 index 0000000..d828be0 --- /dev/null +++ b/commands/flags.go @@ -0,0 +1,488 @@ +package commands + +import ( + "github.com/urfave/cli/v2" + "github.com/urfave/cli/v2/altsrc" + + "rmk/git_handler" + "rmk/system" +) + +func flagsConfig() []cli.Flag { + return []cli.Flag{ + altsrc.NewStringFlag( + &cli.StringFlag{ + Name: "artifact-mode", + Usage: "choice of artifact usage model, available: none, online, offline", + Aliases: []string{"am"}, + EnvVars: []string{"RMK_ARTIFACT_MODE"}, + Value: system.ArtifactModeDefault, + }, + ), + altsrc.NewStringFlag( + &cli.StringFlag{ + Name: "aws-ecr-host", + Usage: "AWS ECR host", + Aliases: []string{"aeh"}, + EnvVars: []string{"RMK_AWS_ECR_HOST"}, + Value: system.AWSECRHost, + }, + ), + altsrc.NewStringFlag( + &cli.StringFlag{ + Name: "aws-ecr-region", + Usage: "AWS region for specific ECR host", + Aliases: []string{"aer"}, + EnvVars: []string{"RMK_AWS_ECR_REGION"}, + Value: system.AWSECRRegion, + }, + ), + altsrc.NewStringFlag( + &cli.StringFlag{ + Name: "aws-ecr-user-name", + Usage: "AWS ECR user name", + Aliases: []string{"aeun"}, + EnvVars: []string{"RMK_AWS_ECR_USER_NAME"}, + Value: system.AWSECRUserName, + }, + ), + altsrc.NewStringFlag( + &cli.StringFlag{ + Name: "aws-mfa-profile", + Hidden: true, + }, + ), + altsrc.NewStringFlag( + &cli.StringFlag{ + Name: "aws-mfa-token-expiration", + Hidden: true, + }, + ), + &cli.BoolFlag{ + Name: "aws-reconfigure", + Usage: "force AWS profile creation", + Aliases: []string{"r"}, + }, + &cli.BoolFlag{ + Name: "aws-reconfigure-artifact-license", + Usage: "force AWS profile creation for artifact license, used only if RMK config option artifact-mode has values: online, offline", + Aliases: []string{"l"}, + }, + altsrc.NewBoolFlag( + &cli.BoolFlag{ + Name: "cluster-provisioner-state-locking", + Usage: "disable or enable cluster provisioner state locking", + Aliases: []string{"c"}, + Value: true, + }, + ), + &cli.StringFlag{ + Name: "config", + Hidden: true, + }, + altsrc.NewStringFlag( + &cli.StringFlag{ + Name: "config-from", + Hidden: true, + }, + ), + &cli.StringFlag{ + Name: "config-from-environment", + Usage: "inheritance of RMK config credentials from environments: " + + git_handler.DefaultDevelop + ", " + git_handler.DefaultStaging + ", " + git_handler.DefaultProduction, + Aliases: []string{"cfe"}, + EnvVars: []string{"RMK_CONFIG_FROM_ENVIRONMENT"}, + }, + altsrc.NewStringFlag( + &cli.StringFlag{ + Name: "github-token", + Usage: "personal access token for download GitHub artifacts", + Aliases: []string{"ght"}, + EnvVars: []string{"RMK_GITHUB_TOKEN"}, + }, + ), + altsrc.NewStringFlag( + &cli.StringFlag{ + Name: "cloudflare-token", + Usage: "Cloudflare API token for provision NS records", + Aliases: []string{"cft"}, + EnvVars: []string{"RMK_CLOUDFLARE_TOKEN"}, + }, + ), + altsrc.NewStringFlag( + &cli.StringFlag{ + Name: "root-domain", + Usage: "domain name for external access to app services via ingress controller", + Aliases: []string{"rd"}, + EnvVars: []string{"RMK_ROOT_DOMAIN"}, + }, + ), + altsrc.NewBoolFlag( + &cli.BoolFlag{ + Name: "progress-bar", + Usage: "globally disable or enable progress bar for download process", + Aliases: []string{"p"}, + Value: true, + }, + ), + altsrc.NewStringFlag( + &cli.StringFlag{ + Name: "s3-charts-repo-region", + Usage: "location constraint region of S3 charts repo", + Aliases: []string{"scrr"}, + EnvVars: []string{"RMK_S3_CHARTS_REPO_REGION"}, + Value: system.S3ChartsRepoRegion, + }, + ), + altsrc.NewStringFlag( + &cli.StringFlag{ + Name: "cluster-provider", + Usage: "select cluster provider to provision clusters", + Aliases: []string{"cp"}, + EnvVars: []string{"RMK_CLUSTER_PROVIDER"}, + Value: system.AWSClusterProvider, + }, + ), + altsrc.NewBoolFlag( + &cli.BoolFlag{ + Name: "slack-notifications", + Usage: "enable Slack notifications", + Aliases: []string{"n"}, + }, + ), + altsrc.NewStringFlag( + &cli.StringFlag{ + Name: "slack-webhook", + Usage: "URL for Slack webhook", + Aliases: []string{"sw"}, + EnvVars: []string{"RMK_SLACK_WEBHOOK"}, + }, + ), + altsrc.NewStringFlag( + &cli.StringFlag{ + Name: "slack-channel", + Usage: "channel name for Slack notification", + Aliases: []string{"sc"}, + EnvVars: []string{"RMK_SLACK_CHANNEL"}, + }, + ), + altsrc.NewStringSliceFlag( + &cli.StringSliceFlag{ + Name: "slack-message-details", + Usage: "additional information for body of Slack message", + Aliases: []string{"smd"}, + EnvVars: []string{"RMK_SLACK_MESSAGE_DETAILS"}, + }, + ), + } +} + +func flagsConfigList() []cli.Flag { + return append(flagsHidden(), + &cli.BoolFlag{ + Name: "all", + Usage: "list all tenant configurations", + Aliases: []string{"a"}, + }, + ) +} + +func flagsClusterK3DCreate() []cli.Flag { + return append(flagsHidden(), + &cli.StringFlag{ + Name: "k3d-volume-host-path", + Usage: "host local directory path for mount into K3D cluster", + Aliases: []string{"kv"}, + EnvVars: []string{"RMK_K3D_VOLUME_HOST_PATH"}, + Value: system.GetPwdPath(""), + }, + ) +} + +func flagsClusterK3DImport() []cli.Flag { + return append(flagsHidden(), + &cli.StringSliceFlag{ + Name: "k3d-import-image", + Usage: "list images for import into K3D cluster", + Aliases: []string{"ki"}, + EnvVars: []string{"RMK_K3D_IMPORT_IMAGE"}, + Required: true, + }, + ) +} + +func flagsClusterCRLogin() []cli.Flag { + return append(flagsHidden(), + &cli.BoolFlag{ + Name: "get-token", + Usage: "get ECR token for authentication", + Aliases: []string{"g"}, + }, + ) +} + +func flagsClusterPlan() []cli.Flag { + return append(flagsHidden(), + &cli.BoolFlag{ + Name: "plan", + Usage: "creates an execution Terraform plan", + Aliases: []string{"p"}, + }, + ) +} + +func flagsClusterStateDelete() []cli.Flag { + return append(flagsHidden(), + &cli.StringFlag{ + Name: "resource-address", + Usage: "resource address for delete from Terraform state", + Aliases: []string{"ra"}, + EnvVars: []string{"RMK_CLUSTER_STATE_RESOURCE_ADDRESS"}, + }, + ) +} + +func flagsClusterSwitch() []cli.Flag { + return append(flagsHidden(), + &cli.BoolFlag{ + Name: "force", + Usage: "force update Kubernetes context from remote cluster", + Aliases: []string{"f"}, + }, + ) +} + +func flagsProjectGenerate() []cli.Flag { + return append(flagsHidden(), + &cli.BoolFlag{ + Name: "create-sops-age-keys", + Usage: "create SOPS age keys for generated project structure", + Aliases: []string{"c"}, + }, + ) +} + +func flagsProjectUpdate() []cli.Flag { + return append(flagsHidden(), + &cli.StringFlag{ + Name: "dependency", + Usage: "specific dependency name for updating project file", + Aliases: []string{"d"}, + Required: true, + EnvVars: []string{"RMK_PROJECT_UPDATE_DEPENDENCY"}, + }, + &cli.BoolFlag{ + Name: "skip-ci", + Usage: "add [skip ci] to commit message line to skip triggering other CI builds", + Aliases: []string{"i"}, + }, + &cli.BoolFlag{ + Name: "skip-commit", + Usage: "only change a version in for project file without committing and pushing it", + Aliases: []string{"c"}, + }, + &cli.StringFlag{ + Name: "version", + Usage: "specific dependency version for updating project file", + Aliases: []string{"v"}, + Required: true, + EnvVars: []string{"RMK_PROJECT_UPDATE_VERSION"}, + }, + ) +} + +func flagsReleaseHelmfile(output bool) []cli.Flag { + flags := flagsHidden() + flags = append(flags, + &cli.StringFlag{ + Name: "helmfile-args", + Usage: "Helmfile additional arguments", + Aliases: []string{"ha"}, + EnvVars: []string{"RMK_RELEASE_HELMFILE_ARGS"}, + Value: "", + }, + &cli.StringFlag{ + Name: "helmfile-log-level", + Usage: "Helmfile log level severity, available: debug, info, warn, error", + Aliases: []string{"hll"}, + EnvVars: []string{"RMK_RELEASE_HELMFILE_LOG_LEVEL"}, + Value: "error", + }, + &cli.StringSliceFlag{ + Name: "selector", + Usage: "only run using releases that match labels. Labels can take form of foo=bar or foo!=bar", + Aliases: []string{"l"}, + EnvVars: []string{"RMK_RELEASE_SELECTOR"}, + }, + &cli.BoolFlag{ + Name: "skip-context-switch", + Usage: "skip context switch for not provisioned cluster", + Aliases: []string{"s"}, + }, + ) + + if output { + flags = append(flags, + &cli.StringFlag{ + Name: "output", + Usage: "output format, available: short, yaml", + Aliases: []string{"o"}, + EnvVars: []string{"RMK_RELEASE_OUTPUT"}, + Value: "short", + }, + ) + } + + return flags +} + +func flagsReleaseRollback() []cli.Flag { + return append(flagsHidden(), + &cli.BoolFlag{ + Name: "skip-context-switch", + Usage: "skip context switch for not provisioned cluster", + Aliases: []string{"s"}, + }, + &cli.StringSliceFlag{ + Name: "release-name", + Usage: "list release names for rollback status in Kubernetes", + Aliases: []string{"rn"}, + Required: true, + }, + ) +} + +func flagsReleaseUpdate() []cli.Flag { + return append(flagsHidden(), + &cli.BoolFlag{ + Name: "commit", + Usage: "only commit and push changes for releases file", + Aliases: []string{"c"}, + }, + &cli.BoolFlag{ + Name: "deploy", + Usage: "deploy updated releases after committed and pushed changes", + Aliases: []string{"d"}, + }, + &cli.StringFlag{ + Name: "repository", + Usage: "specific repository for updating releases file", + Aliases: []string{"r"}, + Required: true, + EnvVars: []string{"RMK_RELEASE_UPDATE_REPOSITORY"}, + }, + &cli.BoolFlag{ + Name: "skip-ci", + Usage: "add [skip ci] to commit message line to skip triggering other CI builds", + Aliases: []string{"i"}, + }, + &cli.BoolFlag{ + Name: "skip-context-switch", + Usage: "skip context switch for not provisioned cluster", + Aliases: []string{"s"}, + }, + &cli.StringFlag{ + Name: "tag", + Usage: "specific tag for updating releases file", + Aliases: []string{"t"}, + Required: true, + EnvVars: []string{"RMK_RELEASE_UPDATE_TAG"}, + }, + ) +} + +func flagsSecretGenerate() []cli.Flag { + return append(flagsSecretManager(), + &cli.BoolFlag{ + Name: "force", + Usage: "force overwriting current secrets after generating new", + Aliases: []string{"f"}, + }, + ) +} + +func flagsSecretManager() []cli.Flag { + return append(flagsHidden(), + &cli.StringSliceFlag{ + Name: "scope", + Usage: "specific scopes for selected secrets", + Aliases: []string{"s"}, + EnvVars: []string{"RMK_SECRET_MANAGER_SCOPE"}, + }, + &cli.StringSliceFlag{ + Name: "environment", + Usage: "specific environments for selected secrets", + Aliases: []string{"e"}, + EnvVars: []string{"RMK_SECRET_MANAGER_ENVIRONMENT"}, + }, + ) +} + +func flagsUpdate() []cli.Flag { + return append(flagsHidden(), + &cli.BoolFlag{ + Name: "release-candidate", + Usage: "force update RMK to latest release candidate version", + Aliases: []string{"r"}, + }, + &cli.StringFlag{ + Name: "version", + Usage: "RMK special version.", + Aliases: []string{"v"}, + DefaultText: "empty value corresponds latest version", + EnvVars: []string{"RMK_UPDATE_VERSION"}, + }, + ) +} + +func flagsHidden() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: "config", + Hidden: true, + }, + altsrc.NewStringFlag( + &cli.StringFlag{ + Name: "artifact-mode", + Hidden: true, + }, + ), + altsrc.NewStringFlag( + &cli.StringFlag{ + Name: "github-token", + Hidden: true, + }, + ), + altsrc.NewStringFlag( + &cli.StringFlag{ + Name: "cloudflare-token", + Hidden: true, + }, + ), + altsrc.NewStringFlag( + &cli.StringFlag{ + Name: "cluster-provider", + Hidden: true, + }, + ), + } +} + +func FlagsGlobal() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: "log-format", + Usage: "log output format, available: console, json", + Aliases: []string{"lf"}, + EnvVars: []string{"RMK_LOG_FORMAT"}, + Value: "console", + }, + &cli.StringFlag{ + Name: "log-level", + Usage: "log level severity, available: debug, info, error", + Aliases: []string{"ll"}, + EnvVars: []string{"RMK_LOG_LEVEL"}, + Value: "info", + }, + } +} diff --git a/commands/k3d_category.go b/commands/k3d_category.go new file mode 100644 index 0000000..6e74beb --- /dev/null +++ b/commands/k3d_category.go @@ -0,0 +1,173 @@ +package commands + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/urfave/cli/v2" + + "rmk/config" + "rmk/system" +) + +type K3DRunner interface { + createDeleteK3DCluster() error + importImageToK3DCluster() error + listK3DClusters() error + startStopK3DCluster() error +} + +type K3DCommands struct { + *ReleaseCommands +} + +func newK3DCommands(conf *config.Config, ctx *cli.Context, workDir string) *K3DCommands { + return &K3DCommands{&ReleaseCommands{Conf: conf, Ctx: ctx, WorkDir: workDir}} +} + +func (k *K3DCommands) k3d(args ...string) *system.SpecCMD { + return &system.SpecCMD{ + Args: append([]string{}, args...), + Command: "k3d", + Dir: k.WorkDir, + Ctx: k.Ctx.Context, + DisableStdOut: false, + Debug: false, + } +} + +func (k *K3DCommands) prepareK3D(args ...string) error { + k.SpecCMD = k.k3d(args...) + k.SpecCMD.Debug = true + credentials, err := k.Conf.AwsConfigure.GetECRCredentials(k.Conf.AWSECRRegion) + if err != nil { + return err + } + + for key, val := range credentials { + k.SpecCMD.Envs = append(k.SpecCMD.Envs, + "K3D_NAME="+k.Conf.Name, + "K3D_VOLUME_HOST_PATH="+k.Ctx.String("k3d-volume-host-path"), + "K3D_AWS_ECR_USER="+key, + "K3D_AWS_ECR_PASSWORD="+val, + ) + } + + return nil +} + +func (k *K3DCommands) createDeleteK3DCluster() error { + var k3dDst string + + k.K3DCluster = true + if _, _, err := k.getKubeContext(); err != nil { + return err + } + + for name, pkg := range k.Conf.Clusters { + if strings.HasPrefix(name, system.K3DConfigPrefix) { + k3dDst = pkg.DstPath + break + } + } + + if len(k3dDst) == 0 { + return fmt.Errorf("cluster provider with name %s not found", system.K3DConfigPrefix) + } + + match, err := system.WalkMatch(k3dDst, system.K3DConfigPrefix+".yaml") + if err != nil { + return err + } + + if len(match) == 0 { + return fmt.Errorf("configuration file for %s not found", system.K3DConfigPrefix) + } + + if err := k.prepareK3D("cluster", k.Ctx.Command.Name, "--config", match[0]); err != nil { + return err + } + + // Creating specific dir for k3d registry configuration + k3dRegistryHostPath := filepath.Join(filepath.Dir(match[0]), system.K3DConfigPrefix) + k.SpecCMD.Envs = append(k.SpecCMD.Envs, "K3D_REGISTRY_HOST_PATH="+k3dRegistryHostPath) + + if err := os.RemoveAll(k3dRegistryHostPath); err != nil { + return err + } + + if err := os.MkdirAll(k3dRegistryHostPath, 0755); err != nil { + return err + } + + return runner(k).runCMD() +} + +func (k *K3DCommands) importImageToK3DCluster() error { + if err := k.prepareK3D(append(append([]string{}, "image", "import", "--cluster", k.Conf.Name, "--keep-tools"), + k.Ctx.StringSlice("k3d-import-image")...)...); err != nil { + return err + } + + return runner(k).runCMD() +} + +func (k *K3DCommands) listK3DClusters() error { + k.K3DCluster = true + if _, _, err := k.getKubeContext(); err != nil { + return err + } + + if err := k.prepareK3D("cluster", k.Ctx.Command.Name); err != nil { + return err + } + + return runner(k).runCMD() +} + +func (k *K3DCommands) startStopK3DCluster() error { + k.K3DCluster = true + if _, _, err := k.getKubeContext(); err != nil { + return err + } + + if err := k.prepareK3D("cluster", k.Ctx.Command.Name, k.Conf.Name); err != nil { + return err + } + + return runner(k).runCMD() +} + +func K3DCreateAction(conf *config.Config) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateArtifactModeDefault(c, ""); err != nil { + return err + } + + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { + return err + } + + return newK3DCommands(conf, c, system.GetPwdPath("")).createDeleteK3DCluster() + } +} + +func K3DAction(conf *config.Config, action func(k3dRunner K3DRunner) error) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateArtifactModeDefault(c, ""); err != nil { + return err + } + + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + return action(newK3DCommands(conf, c, system.GetPwdPath(""))) + } +} diff --git a/commands/project_category.go b/commands/project_category.go new file mode 100644 index 0000000..840cb69 --- /dev/null +++ b/commands/project_category.go @@ -0,0 +1,438 @@ +package commands + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "reflect" + "regexp" + "sort" + "strings" + "text/template" + + "github.com/urfave/cli/v2" + "go.uber.org/zap" + "gopkg.in/yaml.v3" + + "rmk/config" + "rmk/git_handler" + "rmk/notification" + "rmk/system" +) + +type ProjectCommands struct { + *parseContent + projectFile *config.ProjectFile + *projectSpec + *ReleaseCommands +} + +type environment struct { + globalsPath string + releasesPath string + secretsPath string + valuesPath string +} + +type scope struct { + name string + environments map[string]*environment +} + +type parseContent struct { + Dependencies []string + EnvironmentName string + HelmfileParts []string + RepoName string + Scopes []string + TenantName string + TenantNameEnvStyle string +} + +type projectSpec struct { + scopes []scope + owners string +} + +func newProjectCommand(conf *config.Config, ctx *cli.Context, workDir string) *ProjectCommands { + return &ProjectCommands{ + &parseContent{ + TenantName: conf.Tenant, + TenantNameEnvStyle: strings.ToUpper(regexp.MustCompile(`[\-.]`).ReplaceAllString(conf.Tenant, "_")), + }, + &config.ProjectFile{}, + &projectSpec{owners: codeOwners}, + &ReleaseCommands{Conf: conf, Ctx: ctx, WorkDir: workDir}, + } +} + +func (p *ProjectCommands) readProjectFile() error { + if !system.IsExists(system.GetPwdPath(system.TenantProjectFile), true) { + return fmt.Errorf("%s file not found", system.GetPwdPath(system.TenantProjectFile)) + } + + data, err := os.ReadFile(system.GetPwdPath(system.TenantProjectFile)) + if err != nil { + return err + } + + if err := yaml.Unmarshal(data, &p.projectFile); err != nil { + return err + } + + return nil +} + +func (p *ProjectCommands) serializeProjectFile() ([]byte, error) { + var count = 0 + + if err := p.readProjectFile(); err != nil { + return nil, err + } + + for key, pkg := range p.projectFile.Dependencies { + if pkg.Name == p.Ctx.String("dependency") && pkg.Version != p.Ctx.String("version") { + p.projectFile.Dependencies[key].Version = p.Ctx.String("version") + count++ + zap.S().Infof("version changed for dependency %s, affected file: %s", + pkg.Name, + system.GetPwdPath(system.TenantProjectFile)) + break + } + } + + if count == 0 { + zap.S().Infof("version %s for dependency %s is not updated", + p.Ctx.String("version"), p.Ctx.String("dependency")) + return nil, nil + } + + var buf bytes.Buffer + + encoder := yaml.NewEncoder(&buf) + encoder.SetIndent(2) + if err := encoder.Encode(&p.projectFile); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func (p *ProjectCommands) genMsgCommit() string { + msg := fmt.Sprintf("Auto version update %s for dependency: %s", + p.Ctx.String("version"), p.Ctx.String("dependency")) + + if p.Ctx.Bool("skip-ci") { + return fmt.Sprintf("%s %s", "[skip ci]", msg) + } + + return msg +} + +func (p *ProjectCommands) updateProjectFile(gitSpec *git_handler.GitSpec) error { + data, err := p.serializeProjectFile() + if err != nil { + return err + } + + if data != nil { + if err := os.WriteFile(system.GetPwdPath(system.TenantProjectFile), data, 0644); err != nil { + return err + } + + if !p.Ctx.Bool("skip-commit") { + if err := gitSpec.GitCommitPush( + system.GetPwdPath(system.TenantProjectFile), + p.genMsgCommit(), p.Conf.GitHubToken); err != nil { + return err + } + } + + tmp := ¬ification.TmpUpdate{Config: p.Conf, Context: p.Ctx} + tmp.ChangesList = append(tmp.ChangesList, p.Ctx.String("dependency")) + tmp.PathToFile = system.TenantProjectFile + if err := notification.SlackInit(tmp, + notification.SlackTmp(tmp).TmpProjectUpdateMsg()).SlackDeclareNotify(); err != nil { + return err + } + } + + return nil +} + +func (p *ProjectCommands) writeProjectFiles(path, data string) error { + if system.IsExists(path, true) { + zap.S().Warnf("file %s already exists", path) + return nil + } + + if len(data) == 0 { + return nil + } + + if err := os.WriteFile(path, []byte(data), 0644); err != nil { + return err + } + + zap.S().Infof("file %s generated", path) + + return nil +} + +func (p *ProjectCommands) generateReadme(gitSpec *git_handler.GitSpec) error { + p.RepoName = gitSpec.RepoName + + readmeF, err := p.Conf.ParseTemplate(template.New("README"), &p.parseContent, readmeFile) + if err != nil { + return err + } + + if err := p.writeProjectFiles(system.GetPwdPath(system.ReadmeFileName), readmeF); err != nil { + return err + } + + return nil +} + +func (p *ProjectCommands) generateHelmfile() error { + sort.Strings(p.projectFile.Spec.Environments) + + for key, name := range p.projectFile.Spec.Environments { + p.EnvironmentName = name + hEnvironments, err := p.Conf.ParseTemplate(template.New("Helmfile"), &p.parseContent, helmfileEnvironments) + if err != nil { + return err + } + + if key == 0 { + p.HelmfileParts = append(p.HelmfileParts, fmt.Sprintf("environments:\n%s", hEnvironments)) + } else { + p.HelmfileParts = append(p.HelmfileParts, hEnvironments) + } + } + + p.HelmfileParts = append(p.HelmfileParts, helmDefaults) + + hInclude, err := p.Conf.ParseTemplate(template.New("Helmfile"), &p.parseContent, helmfiles) + if err != nil { + return err + } + + p.HelmfileParts = append(p.HelmfileParts, hInclude, helmfileMissingFileHandler) + + hCommonLabels, err := p.Conf.ParseTemplate(template.New("Helmfile"), &p.parseContent, helmfileCommonLabels) + if err != nil { + return err + } + + p.HelmfileParts = append(p.HelmfileParts, hCommonLabels, helmfileTemplates) + + hReleases, err := p.Conf.ParseTemplate(template.New("Helmfile"), &p.parseContent, helmfileReleases) + if err != nil { + return err + } + + p.HelmfileParts = append(p.HelmfileParts, hReleases) + + if err := p.writeProjectFiles(system.GetPwdPath(system.HelmfileGoTmplName), strings.Join(p.HelmfileParts, "\n")); err != nil { + return err + } + + return nil +} + +func (p *ProjectCommands) generateProjectFiles(gitSpec *git_handler.GitSpec) error { + for _, sc := range p.scopes { + for _, env := range sc.environments { + switch sc.name { + case "clusters": + if err := p.writeProjectFiles(filepath.Join(env.valuesPath, system.TerraformVarsFile), clusterVariables); err != nil { + return err + } + + if err := p.writeProjectFiles(filepath.Join(env.valuesPath, system.TerraformWGFile), clusterWorkerGroups); err != nil { + return err + } + case p.TenantName: + tGlobals, err := p.Conf.ParseTemplate(template.New("TenantGlobals"), &p.parseContent, tenantGlobals) + if err != nil { + return err + } + + if err := p.writeProjectFiles(env.globalsPath, tGlobals); err != nil { + return err + } + + tReleases, err := p.Conf.ParseTemplate(template.New("TenantReleases"), &p.parseContent, tenantReleasesFile) + if err != nil { + return err + } + + if err := p.writeProjectFiles(env.releasesPath, tReleases); err != nil { + return err + } + + tSecretSpec, err := p.Conf.ParseTemplate(template.New("TenantSecretSpec"), &p.parseContent, tenantSecretSpecFile) + if err != nil { + return err + } + + if err := p.writeProjectFiles(filepath.Join(env.secretsPath, system.SecretSpecFile), tSecretSpec); err != nil { + return err + } + + if err := p.writeProjectFiles(filepath.Join(env.valuesPath, p.TenantName+"-app.yaml.gotmpl"), tenantValuesExample); err != nil { + return err + } + default: + if err := p.writeProjectFiles(env.globalsPath, globals); err != nil { + return err + } + + if err := p.writeProjectFiles(env.releasesPath, releasesFile); err != nil { + return err + } + + if err := p.writeProjectFiles(filepath.Join(env.secretsPath, system.SecretSpecFile), secretSpecFile); err != nil { + return err + } + } + + if sc.name != "clusters" { + if err := p.writeProjectFiles(filepath.Join(env.secretsPath, system.SopsConfigFile), sopsConfigFile); err != nil { + return err + } + } + } + } + + if err := p.writeProjectFiles(system.GetPwdPath(system.TenantProjectGitIgn), gitignore); err != nil { + return err + } + + if err := p.writeProjectFiles(system.GetPwdPath(system.TenantProjectCodeOwners), p.owners); err != nil { + return err + } + + if err := p.generateHelmfile(); err != nil { + return err + } + + if err := p.generateReadme(gitSpec); err != nil { + return err + } + + return nil +} + +func (p *ProjectCommands) generateProject(gitSpec *git_handler.GitSpec) error { + if err := p.readProjectFile(); err != nil { + return err + } + + for _, pkg := range p.projectFile.Dependencies { + p.Dependencies = append(p.Dependencies, pkg.Name) + } + + if len(p.projectFile.Spec.Owners) > 0 { + p.owners = p.owners + fmt.Sprintf("* @%s\n", strings.Join(p.projectFile.Spec.Owners, " @")) + } else { + p.owners = "" + } + + if reflect.ValueOf(p.projectFile.Spec).IsZero() { + return fmt.Errorf("'spec' option required in %s", system.TenantProjectFile) + } + + switch { + case len(p.projectFile.Spec.Scopes) == 0 && len(p.projectFile.Spec.Environments) > 0: + return fmt.Errorf("'scopes' option required, if 'environments' specified in %s", system.TenantProjectFile) + case len(p.projectFile.Spec.Scopes) > 0 && len(p.projectFile.Spec.Environments) == 0: + return fmt.Errorf("'environments' option required, if 'scopes' specified in %s", system.TenantProjectFile) + case len(p.projectFile.Spec.Scopes) == 0 && len(p.projectFile.Spec.Environments) == 0: + return fmt.Errorf("'scopes', 'environments' options required in %s", system.TenantProjectFile) + } + + for sKey, sc := range p.projectFile.Spec.Scopes { + p.Scopes = append(p.Scopes, sc) + p.scopes = append(p.scopes, scope{name: sc, environments: make(map[string]*environment)}) + for _, env := range p.projectFile.Spec.Environments { + if sc == "clusters" { + p.scopes[sKey].environments[env] = &environment{ + secretsPath: system.GetPwdPath(system.TenantValuesDIR, sc, p.Conf.ClusterProvider, env, "secrets"), + valuesPath: system.GetPwdPath(system.TenantValuesDIR, sc, p.Conf.ClusterProvider, env, "values"), + } + + continue + } + + p.scopes[sKey].environments[env] = &environment{ + globalsPath: system.GetPwdPath(system.TenantValuesDIR, sc, env, system.GlobalsFileName), + releasesPath: system.GetPwdPath(system.TenantValuesDIR, sc, env, system.ReleasesFileName), + secretsPath: system.GetPwdPath(system.TenantValuesDIR, sc, env, "secrets"), + valuesPath: system.GetPwdPath(system.TenantValuesDIR, sc, env, "values"), + } + } + } + + for _, sc := range p.scopes { + for _, env := range sc.environments { + if err := os.MkdirAll(env.secretsPath, 0755); err != nil { + return err + } + + if err := os.MkdirAll(env.valuesPath, 0755); err != nil { + return err + } + } + } + + if err := os.MkdirAll(system.GetPwdPath("docs"), 0755); err != nil { + return err + } + + if err := p.generateProjectFiles(gitSpec); err != nil { + return err + } + + if p.Ctx.Bool("create-sops-age-keys") { + if err := newSecretCommands(p.Conf, p.Ctx, system.GetPwdPath()).CreateKeys(); err != nil { + return err + } + } + + return nil +} + +func projectGenerateAction(conf *config.Config, gitSpec *git_handler.GitSpec) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateArtifactModeDefault(c, ""); err != nil { + return err + } + + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + if err := newProjectCommand(conf, c, system.GetPwdPath()).generateProject(gitSpec); err != nil { + return err + } + + return resolveDependencies(conf.InitConfig(false), c, false) + } +} + +func projectUpdateAction(conf *config.Config, gitSpec *git_handler.GitSpec) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateArtifactModeDefault(c, ""); err != nil { + return err + } + + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + return newProjectCommand(conf, c, system.GetPwdPath()).updateProjectFile(gitSpec) + } +} diff --git a/commands/project_generation_category.go b/commands/project_generation_category.go new file mode 100644 index 0000000..c730f68 --- /dev/null +++ b/commands/project_generation_category.go @@ -0,0 +1,411 @@ +package commands + +const ( + clusterVariables = `# Kubernetes user list +k8s_master_usernames = [] +k8s_cluster_version = "" # Actual EKS Kubernetes version +# Bastion +bastion_enabled = false +# IAM Roles +aws_lb_controller_role_enabled = true +external_dns_role_enabled = true +ebs_csi_controller_roles_enabled = true +` + + clusterWorkerGroups = `# For example: +# worker_groups = [ +# { +# name = "" +# instance_type = "t3.medium" +# additional_userdata = "t3.medium" +# asg_desired_capacity = 3 +# asg_max_size = 3 +# asg_min_size = 3 +# ami_id = "ami-" +# kubelet_extra_args = "--node-labels=key1=value1" +# root_volume_size = "10" +# root_volume_type = "gp3" +# enable_monitoring = false +# }, +# { +# name = "" +# instance_type = "t3.medium" +# additional_userdata = "t3.medium" +# asg_desired_capacity = 3 +# asg_max_size = 3 +# asg_min_size = 3 +# ami_id = "ami-" +# kubelet_extra_args = "--node-labels=key2=value2 --register-with-taints=key2=value2:NoSchedule" +# root_volume_size = "10" +# root_volume_type = "gp3" +# enable_monitoring = false +# }, +# ] + +worker_groups = [] +` + + codeOwners = `# These owners will be the default owners for everything in +# the repo and will be requested for review when someone opens a pull request. +` + + escape = "`" + escapeOpen = `{{` + "`" + escapeClose = "`" + `}}` + + gitignore = `**.dec +*/cluster/k3d/ +dist/ +.DS_Store +.PROJECT/ +.deps/ +.env +.helmfile/ +.idea/ +sops-age-keys/ +` + + globals = `# This file defines the globals configuration list for values different releases, +# is located in the environment directory of a specific releases scope: etc///globals.yaml.gotmpl. +# This configuration allows you to use the same values in value files of different releases only in the same scope. + +# configs - enumeration of configurations divided into sets related to Kubernetes ConfigMaps +configs: {} + +# envs - enumeration of environment variables divided into sets related to Kubernetes environment variables for container +envs: {} + +# hooks - enumeration of environment variables divided into sets related to helmfile hooks arguments +hooks: {} +` + + helmfileEnvironments = ` {{ .EnvironmentName }}: + missingFileHandler: Warn + values: + - etc/{{ .TenantName }}/` + escapeOpen + `{{ .Environment.Name }}` + escapeClose + `/globals.yaml + - etc/{{ .TenantName }}/` + escapeOpen + `{{ .Environment.Name }}` + escapeClose + `/globals.yaml.gotmpl + - etc/{{ .TenantName }}/` + escapeOpen + `{{ .Environment.Name }}` + escapeClose + `/releases.yaml + ` + escapeOpen + `{{- if eq (env "K3D_CLUSTER") "true" }}` + escapeClose + ` + - etc/{{ .TenantName }}/` + escapeOpen + `{{ .Environment.Name }}` + escapeClose + `/values/k3d/releases.yaml + ` + escapeOpen + `{{- end }}` + escapeClose + ` + - ` + escapeOpen + `{{ requiredEnv "PWD" }}` + escapeClose + `/etc/{{ .TenantName }}/` + escapeOpen + `{{ .Environment.Name }}` + escapeClose + `/globals.yaml + - ` + escapeOpen + `{{ requiredEnv "PWD" }}` + escapeClose + `/etc/{{ .TenantName }}/` + escapeOpen + `{{ .Environment.Name }}` + escapeClose + `/globals.yaml.gotmpl + - ` + escapeOpen + `{{ requiredEnv "PWD" }}` + escapeClose + `/etc/{{ .TenantName }}/` + escapeOpen + `{{ .Environment.Name }}` + escapeClose + `/releases.yaml + ` + escapeOpen + `{{- if eq (env "K3D_CLUSTER") "true" }}` + escapeClose + ` + - ` + escapeOpen + `{{ requiredEnv "PWD" }}` + escapeClose + `/etc/{{ .TenantName }}/` + escapeOpen + `{{ .Environment.Name }}` + escapeClose + `/values/k3d/releases.yaml + ` + escapeOpen + `{{- end }}` + escapeClose + helmDefaults = `--- +helmDefaults: + wait: true + waitForJobs: true + timeout: 3600 +` + + helmfiles = `# The set of paths for inherited helmfiles is controlled through the version.yaml file using rmk. +# DO NOT EDIT field helmfiles values. +helmfiles: ` + escapeOpen + `{{ env "HELMFILE_` + escapeClose + `{{ .TenantNameEnvStyle }}` + escapeOpen + `_PATHS" }}` + escapeClose + ` +` + + helmfileMissingFileHandler = `missingFileHandler: Warn +` + + helmfileCommonLabels = `commonLabels: + scope: {{ .TenantName }} + namespace: {{ .TenantName }} + bin: ` + escapeOpen + `{{ env "HELMFILE_` + escapeClose + `{{ .TenantNameEnvStyle }}` + escapeOpen + `_HOOKS_DIR" | default (env "HELMFILE_HOOKS_DIR") }}/bin` + escapeClose + ` + repo: core-charts + appChartVersion: 1.6.0 + host: ` + escapeOpen + `{{ env "ROOT_DOMAIN" }}` + escapeClose + ` +` + + helmfileTemplates = `templates: + release: + createNamespace: true + labels: + app: "{{` + escape + `{{ .Release.Name }}` + escape + `}}" + missingFileHandler: Warn + values: + - etc/{{` + escape + `{{ .Release.Labels.scope }}` + escape + `}}/{{` + escape + `{{ .Environment.Name }}` + escape + `}}/values/{{` + escape + `{{ .Release.Name }}` + escape + `}}-ingress-route.yaml.gotmpl + - etc/{{` + escape + `{{ .Release.Labels.scope }}` + escape + `}}/{{` + escape + `{{ .Environment.Name }}` + escape + `}}/values/{{` + escape + `{{ .Release.Name }}` + escape + `}}.yaml.gotmpl + - etc/{{` + escape + `{{ .Release.Labels.scope }}` + escape + `}}/{{` + escape + `{{ .Environment.Name }}` + escape + `}}/values/{{` + escape + `{{ .Release.Name }}` + escape + `}}.yaml + {{- if eq (env "K3D_CLUSTER") "true" }} + - etc/{{` + escape + `{{ .Release.Labels.scope }}` + escape + `}}/{{` + escape + `{{ .Environment.Name }}` + escape + `}}/values/k3d/values/{{` + escape + `{{ .Release.Name }}` + escape + `}}-ingress-route.yaml.gotmpl + - etc/{{` + escape + `{{ .Release.Labels.scope }}` + escape + `}}/{{` + escape + `{{ .Environment.Name }}` + escape + `}}/values/k3d/values/{{` + escape + `{{ .Release.Name }}` + escape + `}}.yaml.gotmpl + - etc/{{` + escape + `{{ .Release.Labels.scope }}` + escape + `}}/{{` + escape + `{{ .Environment.Name }}` + escape + `}}/values/k3d/values/{{` + escape + `{{ .Release.Name }}` + escape + `}}.yaml + {{- end }} + - {{ requiredEnv "PWD" }}/etc/{{` + escape + `{{ .Release.Labels.scope }}` + escape + `}}/{{` + escape + `{{ .Environment.Name }}` + escape + `}}/values/{{` + escape + `{{ .Release.Name }}` + escape + `}}-ingress-route.yaml.gotmpl + - {{ requiredEnv "PWD" }}/etc/{{` + escape + `{{ .Release.Labels.scope }}` + escape + `}}/{{` + escape + `{{ .Environment.Name }}` + escape + `}}/values/{{` + escape + `{{ .Release.Name }}` + escape + `}}.yaml.gotmpl + - {{ requiredEnv "PWD" }}/etc/{{` + escape + `{{ .Release.Labels.scope }}` + escape + `}}/{{` + escape + `{{ .Environment.Name }}` + escape + `}}/values/{{` + escape + `{{ .Release.Name }}` + escape + `}}.yaml + {{- if eq (env "K3D_CLUSTER") "true" }} + - {{ requiredEnv "PWD" }}/etc/{{` + escape + `{{ .Release.Labels.scope }}` + escape + `}}/{{` + escape + `{{ .Environment.Name }}` + escape + `}}/values/k3d/values/{{` + escape + `{{ .Release.Name }}` + escape + `}}-ingress-route.yaml.gotmpl + - {{ requiredEnv "PWD" }}/etc/{{` + escape + `{{ .Release.Labels.scope }}` + escape + `}}/{{` + escape + `{{ .Environment.Name }}` + escape + `}}/values/k3d/values/{{` + escape + `{{ .Release.Name }}` + escape + `}}.yaml.gotmpl + - {{ requiredEnv "PWD" }}/etc/{{` + escape + `{{ .Release.Labels.scope }}` + escape + `}}/{{` + escape + `{{ .Environment.Name }}` + escape + `}}/values/k3d/values/{{` + escape + `{{ .Release.Name }}` + escape + `}}.yaml + {{- end }} + secrets: + - {{ requiredEnv "PWD" }}/etc/{{` + escape + `{{ .Release.Labels.scope }}` + escape + `}}/{{` + escape + `{{ .Environment.Name }}` + escape + `}}/secrets/{{` + escape + `{{ .Release.Name }}` + escape + `}}.yaml +` + + helmfileReleases = `releases: + # TODO: It is recommended to adapt this example considering security, performance and configuration management + # TODO: requirements specific to your application or infrastructure. + # Group 1 + - name: {{ .TenantName }}-app + namespace: {{ .TenantName }} + chart: "{{"{{` + escape + `{{ .Release.Labels.repo }}` + escape + `}}"}}/app" + version: "{{"{{` + escape + `{{ .Release.Labels.appChartVersion }}` + escape + `}}"}}" + installed: ` + escapeOpen + `{{ .Values | get (print "` + escapeClose + `{{ .TenantName }}-app` + escapeOpen + `" ".enabled") false }}` + escapeClose + ` + inherit: + - template: release + ` + escapeOpen + `{{- with .Values | get (print "hooks." "` + escapeClose + `{{ .TenantName }}-app` + escapeOpen + `") "" }}` + escapeClose + ` + hooks: # common-postuninstall-hook is needed only for the first service in namespace + ` + escapeOpen + `{{- range $name, $hook := . }}` + escapeClose + ` + - name: ` + escapeOpen + `{{ $name }}` + escapeClose + ` + ` + escapeOpen + `{{- toYaml $hook | nindent 8 }}` + escapeClose + ` + ` + escapeOpen + `{{- end }}` + escapeClose + ` + ` + escapeOpen + `{{- end }}` + escapeClose + ` +` + + readmeFile = `# {{ .RepoName }} + +## Description + +The repository designed for the rapid setup and deployment of the infrastructure required for the {{ .TenantName }} project. +This project includes scripts, configurations, and instructions to automate the deployment of necessary services and dependencies. + +## Getting Started + +To get started with {{ .RepoName }}, ensure you have all the necessary tools and dependencies installed. +Detailed information about requirements and installation instructions can be found in the [Requirements](#requirements) section. + +### Requirements + +- AWS CLI >= 2.9 +- AWS IAM user security credentials (access key pair) +- Git +- GitHub PAT to access the following repositories to list in the sections ` + "`project.yaml`" + `: + * clusters + * hooks +- Note: K3D v5.x.x requires at least Docker v20.10.5 (runc >= v1.0.0-rc93) to work properly +- Python >= 3.9 +- [RMK CLI](https://github.com/edenlabllc/rmk.tools.infra?tab=readme-ov-file#rmk-cli---reduced-management-for-kubernetes) >= 4.0.0 + +### GitLab flow strategy + +This repository uses the Environment branches with GitLab flow approach, +where each stable or ephemeral branches is a separate environment with its own Kubernetes cluster. + +Release schema: +` + "```" + `text +develop ------> staging ------> production + \ / \ / + release/vN.N.N-rc release/vN.N.N +` + "```" + ` + +### Generating the Project Structure + +> Note: The generated project structure using the RMK tools is mandatory and is required for the interaction of the RMK with the code base. +> All generated files have example content and can be supplemented according to project requirements. + +After generating the project structure, a set of files is generated for the main project scope etc/{{ .TenantName }} +to demonstrate an example of configuring the {{ .TenantName }}-app release. +This example shows how the following options are configured and interact with each other: + +- etc/{{ .TenantName }}/\/secrets/.spec.yaml.gotmpl +- etc/{{ .TenantName }}/\/values/rmk-test-app.yaml.gotmpl +- etc/{{ .TenantName }}/\/globals.yaml.gotmpl +- etc/{{ .TenantName }}/\/releases.yaml +- helmfile.yaml.gotmpl + +{{ if .Dependencies }} +#### Inherited repositories +{{ range .Dependencies }} +- **{{ . }}** +{{ end }} +{{- end }} +{{- if .Scopes }} +#### Available scopes of variables +{{ range .Scopes }} +- **{{ . }}** +{{ end }} +{{- end }} +### Basic RMK commands for project management + +#### Initialization configuration + +` + "```" + `shell +rmk config init +` + "```" + ` + +#### Cluster provision + +` + "```" + `shell +rmk cluster provision --plan +rmk cluster provision +` + "```" + ` + +#### Release sync + +` + "```" + `shell +rmk release sync +` + "```" + ` + +> Note: A complete list of RMK commands and capabilities can be found at the [link](https://github.com/edenlabllc/rmk.tools.infra?tab=readme-ov-file#rmk-cli---reduced-management-for-kubernetes) +` + + releasesFile = `# This file defines the release list, is located in the environment directory +# of a specific releases scope: etc///releases.yaml. +# The absence of this file in the environment directory means that the entire list of releases will be installed. +# Set false to uninstall this release on sync. +` + + secretSpecFile = `# This file defines the generation-rules list for secrets, is located in the secrets directory +# of a specific environment of a specific releases scope: etc///secrets/.spec.yaml.gotmpl. +# This template allows you to generate a new set of secrets, thereby rotating existing secrets. + +generation-rules: [] +` + + sopsConfigFile = `creation_rules: [] +` + + tenantGlobals = `# This file defines the globals configuration list for values different releases, +# is located in the environment directory of a specific releases scope: etc///globals.yaml.gotmpl. +# This configuration allows you to use the same values in value files of different releases only in the same scope. + +# TODO: It is recommended to adapt this example considering security, performance and configuration management +# TODO: requirements specific to your application or infrastructure. +# configs - enumeration of configurations divided into sets related to Kubernetes ConfigMaps +configs: + containerRegistryAuth: + imagePullSecrets: + - name: container-registry-credentials + linkerd: + # enable/disable linkerd sidecar injection: enabled|disabled + inject: enabled + +# envs - enumeration of environment variables divided into sets related to Kubernetes environment variables for container +envs: + logger: + LOG_LEVEL: "info" + +# hooks - enumeration of environment variables divided into sets related to helmfile hooks arguments +hooks: + {{ .TenantName }}-app: + common-postuninstall-hook: + events: + - postuninstall + showlogs: true + command: "{{"{{` + escape + `{{ .Release.Labels.bin }}` + escape + `}}"}}/common-postuninstall-hook.sh" + args: + - "{{"{{` + escape + `{{ .Release.Namespace }}` + escape + `}}"}}" +` + + tenantReleasesFile = releasesFile + ` +# TODO: It is recommended to adapt this example considering security, performance and configuration management +# TODO: requirements specific to your application or infrastructure. +{{ .TenantName }}-app: + enabled: true + image: + repository: nginx + tag: latest +` + + tenantSecretSpecFile = `# This file defines the generation-rules list for secrets, is located in the secrets directory +# of a specific environment of a specific releases scope: etc///secrets/.spec.yaml.gotmpl. +# This template allows you to generate a new set of secrets, thereby rotating existing secrets. + +# TODO: It is recommended to adapt this example considering security, performance and configuration management +# TODO: requirements specific to your application or infrastructure. +generation-rules: + - name: {{ .TenantName }}-app + template: | + envSecret: + USERNAME: user + PASSWORD: ` + escapeOpen + `{{ randAlphaNum 16 }}` + escapeClose + ` +` + + tenantValuesExample = `# This value file is an introductory example configuration for running Nginx in Kubernetes via RMK. +# It combines several key components such as Deployment, Service, ConfigMap, Secrets and other options. +# The value file is intended to demonstrate the basic capabilities of RMK in deploying releases +# and should not be used as is in a production environment. + +# TODO: It is recommended to adapt this example considering security, performance and configuration management +# TODO: requirements specific to your application or infrastructure. +replicaCount: 1 +image: + repository: '{{ .Values | get (printf "%s.image.repository" .Release.Name) }}' + tag: '{{ .Values | get (printf "%s.image.tag" .Release.Name) }}' +imagePullSecrets: + {{ .Values | get "configs.containerRegistryAuth.imagePullSecrets" list | toYaml | nindent 2 }} +jaegerAgent: + enabled: false +podAnnotations: + linkerd.io/inject: {{ "" | default .Values.configs.linkerd.inject }} +envFrom: [] +# - secretRef: +# name: app-secret +command: [] +# - /app/server +env: + ################################################################################# + # Logger settings + ################################################################################# + # Log severity + {{ .Values | get "envs.logger" | toYaml | nindent 2 }} +volumeMounts: + - name: config + mountPath: /etc/nginx/conf.d +volumesConfigMap: + enable: true + volumes: + - name: config + files: + - name: default.conf + data: | + server { + listen 80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + } +ports: + - name: http + containerPort: 80 +service: + enable: true + type: ClusterIP + ports: + - port: 80 + name: http +readinessProbe: + timeoutSeconds: 10 + periodSeconds: 10 + failureThreshold: 3 + httpGet: + path: / + port: 80 +livenessProbe: + initialDelaySeconds: 60 + timeoutSeconds: 10 + periodSeconds: 10 + failureThreshold: 3 + httpGet: + path: / + port: 80 +resources: + limits: + cpu: 100m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi +` +) diff --git a/commands/release_category.go b/commands/release_category.go new file mode 100644 index 0000000..8d34020 --- /dev/null +++ b/commands/release_category.go @@ -0,0 +1,750 @@ +package commands + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" + "time" + + "github.com/urfave/cli/v2" + "go.uber.org/zap" + "gopkg.in/yaml.v3" + "mvdan.cc/sh/v3/shell" + + "rmk/config" + "rmk/git_handler" + "rmk/notification" + "rmk/system" +) + +type runner interface { + runCMD() error +} + +type ReleaseCommands struct { + Conf *config.Config + Ctx *cli.Context + SpecCMD *system.SpecCMD + Scope string + WorkDir string + ValuesPath string + UpdateContext bool + K3DCluster bool +} + +type ReleaseStruct struct { + Enabled bool + Image struct { + Repository string + Tag string + } `yaml:"image,omitempty"` +} + +type ReleasesList struct { + NodeYAML yaml.Node + Releases map[string]*ReleaseStruct + Changes struct { + List map[string][]string + Count int64 + } +} + +type SpecRelease struct { + ReleaseCommands + ReleasesList + ReleasesPaths []string +} + +type HelmfileList []struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + Enabled bool `json:"enabled"` + Installed bool `json:"installed"` + Labels string `json:"labels"` + Chart string `json:"chart"` + Version string `json:"version"` +} + +type HelmStatus struct { + Name string `json:"name"` + Info struct { + FirstDeployed time.Time `json:"first_deployed"` + LastDeployed time.Time `json:"last_deployed"` + Deleted string `json:"deleted"` + Description string `json:"description"` + Status string `json:"status"` + Notes string `json:"notes"` + } `json:"info"` + Version int `json:"version"` + Namespace string `json:"namespace"` +} + +type KubeConfig struct { + Kind string `json:"kind"` + ApiVersion string `json:"apiVersion"` + Preferences struct { + } `json:"preferences"` + Clusters []struct { + Name string `json:"name"` + Cluster struct { + Server string `json:"server"` + CertificateAuthorityData string `json:"certificate-authority-data"` + } `json:"cluster"` + } `json:"clusters"` + Users []struct { + Name string `json:"name"` + User struct { + Exec struct { + Command string `json:"command"` + Args []string `json:"args"` + Env []struct { + Name string `json:"name"` + Value string `json:"value"` + } `json:"env"` + ApiVersion string `json:"apiVersion"` + ProvideClusterInfo bool `json:"provideClusterInfo"` + } `json:"exec"` + } `json:"user"` + } `json:"users"` + Contexts []struct { + Name string `json:"name"` + Context struct { + Cluster string `json:"cluster"` + User string `json:"user"` + } `json:"context"` + } `json:"contexts"` + CurrentContext string `json:"current-context"` +} + +func (rc *ReleaseCommands) runCMD() error { + if err := rc.SpecCMD.AddEnv(); err != nil { + return err + } + + if err := rc.SpecCMD.ExecCMD(); err != nil { + if rc.SpecCMD.Debug { + zap.S().Debugf("command: %s", rc.SpecCMD.CommandStr) + zap.S().Debugf("path: %s", rc.SpecCMD.Dir) + for _, val := range rc.SpecCMD.Envs { + zap.S().Debugf("env: %s", strings.ReplaceAll(val, rc.Conf.GitHubToken, "[rmk_sensitive]")) + } + } + + return err + } + + if rc.SpecCMD.Debug { + zap.S().Debugf("command: %s", rc.SpecCMD.CommandStr) + zap.S().Debugf("path: %s", rc.SpecCMD.Dir) + for _, val := range rc.SpecCMD.Envs { + zap.S().Debugf("env: %s", strings.ReplaceAll(val, rc.Conf.GitHubToken, "[rmk_sensitive]")) + } + } + + return nil +} + +func (rc *ReleaseCommands) nestedHelmfiles(envs ...string) []string { + var hfPath, hfVersion []string + + for _, val := range rc.Conf.Dependencies { + hfPath = append(hfPath, fmt.Sprintf(`{"path":"%s"}`, val.DstPath)) + keyVerEnv := regexp.MustCompile(`[\-.]`).ReplaceAllString(val.Name, "_") + hfVersion = append(hfVersion, "HELMFILE_"+strings.ToUpper(keyVerEnv)+"_VERSION="+val.Version) + } + + keyTenantEnv := regexp.MustCompile(`[\-.]`).ReplaceAllString(rc.Conf.Tenant, "_") + envs = append(envs, "HELMFILE_"+strings.ToUpper(keyTenantEnv)+"_PATHS=["+strings.Join(hfPath, ",")+"]") + return append(envs, hfVersion...) +} + +func (rc *ReleaseCommands) prepareHelmfile(args ...string) *system.SpecCMD { + envs := append([]string{}, + "NAME="+rc.Conf.Name, + "TENANT="+rc.Conf.Tenant, + "SOPS_AGE_KEY_FILE="+filepath.Join(rc.Conf.SopsAgeKeys, system.SopsAgeKeyFile), + "GITHUB_TOKEN="+rc.Conf.GitHubToken, + "AWS_PROFILE="+rc.Conf.Profile, + "AWS_CONFIG_FILE="+strings.Join(rc.Conf.AWSSharedConfigFile(rc.Conf.Profile), ""), + "AWS_SHARED_CREDENTIALS_FILE="+strings.Join(rc.Conf.AWSSharedCredentialsFile(rc.Conf.Profile), ""), + // Needed to set the AWS region to force the connection session region for the helm S3 plugin, + // if AWS_DEFAULT_REGION and AWS_REGION cannot be trusted. + "HELM_S3_REGION="+rc.Conf.S3ChartsRepoRegion, + ) + + if _, ok := rc.Conf.Env["ROOT_DOMAIN"]; ok { + envs = append(envs, "ROOT_DOMAIN="+rc.Conf.Env["ROOT_DOMAIN"]) + delete(rc.Conf.Env, "ROOT_DOMAIN") + } else { + envs = append(envs, "ROOT_DOMAIN="+rc.Conf.RootDomain) + } + + for _, val := range rc.Conf.HooksMapping { + keyTenantEnv := regexp.MustCompile(`[\-.]`).ReplaceAllString(val.Tenant, "_") + envs = append(envs, "HELMFILE_"+strings.ToUpper(keyTenantEnv)+"_HOOKS_DIR="+val.DstPath) + } + + for key, val := range rc.Conf.Env { + envs = append(envs, key+"="+val) + } + + // generating additional environment variables to nested helmfiles + envs = rc.nestedHelmfiles(envs...) + + if rc.K3DCluster { + envs = append(envs, "K3D_CLUSTER="+strconv.FormatBool(rc.K3DCluster)) + } + + // needed if not used artifact mode + var sensKeyWords []string + if rc.Ctx.String("artifact-mode") == system.ArtifactModeDefault { + sensKeyWords = []string{rc.Conf.GitHubToken} + } + + return &system.SpecCMD{ + Args: append([]string{"--environment", rc.Conf.Environment, "--log-level", + rc.Ctx.String("helmfile-log-level")}, args...), + Command: "helmfile", + Ctx: rc.Ctx.Context, + Dir: rc.WorkDir, + Envs: envs, + Debug: true, + SensKeyWords: sensKeyWords, + } +} + +func (rc *ReleaseCommands) kubeConfig() *system.SpecCMD { + return &system.SpecCMD{ + Args: []string{"config"}, + Command: "kubectl", + Ctx: rc.Ctx.Context, + Dir: rc.WorkDir, + DisableStdOut: true, + Debug: false, + } +} + +func (rc *ReleaseCommands) releaseMiddleware() error { + if len(rc.Conf.Dependencies) == 0 && rc.Ctx.String("artifact-mode") == system.ArtifactModeDefault { + if err := os.RemoveAll(filepath.Join(rc.WorkDir, TenantPrDependenciesDir)); err != nil { + return err + } + } + + if err := system.MergeAgeKeys(rc.Conf.SopsAgeKeys); err != nil { + return err + } + + if _, currentContext, err := rc.getKubeContext(); err != nil { + return err + } else { + if strings.Contains(currentContext, system.K3DConfigPrefix) { + rc.K3DCluster = true + } + } + + return nil +} + +func (rc *ReleaseCommands) releaseHelmfile(args ...string) error { + if err := rc.releaseMiddleware(); err != nil { + return err + } + + rc.SpecCMD = rc.prepareHelmfile(args...) + + return rc.runCMD() +} + +func (rc *ReleaseCommands) getKubeContext() (string, string, error) { + var contextName string + kubeConfig := &KubeConfig{} + + rc.SpecCMD = rc.kubeConfig() + rc.SpecCMD.Args = append(rc.SpecCMD.Args, "view", "--output", "json") + if err := rc.runCMD(); err != nil { + return "", "", fmt.Errorf("Kubectl config failed to view\n%s", rc.SpecCMD.StderrBuf.String()) + } + + if err := json.Unmarshal(rc.SpecCMD.StdoutBuf.Bytes(), &kubeConfig); err != nil { + return "", "", err + } + + re, err := regexp.Compile(`(?i)` + rc.Conf.Name + `\b`) + if err != nil { + return "", "", err + } + + for _, val := range kubeConfig.Contexts { + if re.MatchString(val.Name) { + contextName = val.Name + break + } + } + + if rc.K3DCluster && len(contextName) > 0 && !strings.Contains(contextName, system.K3DConfigPrefix) { + return "", "", fmt.Errorf("remote Kubernetes context already exists %s for this branch", contextName) + } + + return contextName, kubeConfig.CurrentContext, nil +} + +func (rc *ReleaseCommands) releaseKubeContext() error { + contextName, currentContextName, err := rc.getKubeContext() + if err != nil { + return err + } + + if len(contextName) > 0 && !rc.UpdateContext { + if contextName != currentContextName { + rc.SpecCMD = rc.kubeConfig() + rc.SpecCMD.Args = append(rc.SpecCMD.Args, "use", contextName) + rc.SpecCMD.DisableStdOut = false + return rc.runCMD() + } + + return nil + } + + cc := &ClusterCommands{ + Conf: rc.Conf, + Ctx: rc.Ctx, + WorkDir: system.GetPwdPath(""), + } + + if err := cc.clusterContext(); err != nil { + return err + } + + _, currentContext, err := rc.getKubeContext() + if err != nil { + return err + } + + rc.SpecCMD = rc.kubeConfig() + rc.SpecCMD.Args = append(rc.SpecCMD.Args, + "set-credentials", currentContext, + "--exec-env", "AWS_CONFIG_FILE="+strings.Join(rc.Conf.AWSSharedConfigFile(cc.Conf.Profile), ""), + "--exec-env", "AWS_SHARED_CREDENTIALS_FILE="+strings.Join(rc.Conf.AWSSharedCredentialsFile(cc.Conf.Profile), ""), + ) + rc.SpecCMD.DisableStdOut = true + rc.SpecCMD.Debug = true + return rc.runCMD() +} + +func (sr *SpecRelease) searchReleasesPath() error { + paths, err := system.WalkInDir(system.GetPwdPath(system.TenantValuesDIR), sr.Conf.Environment, system.ReleasesFileName) + if err != nil { + return err + } + + sr.ReleasesPaths = paths + return nil +} + +func (sr *SpecRelease) readReleasesFile(path string) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + + if err := yaml.Unmarshal(data, &sr.NodeYAML); err != nil { + return err + } + + if err := sr.NodeYAML.Decode(&sr.Releases); err != nil { + return err + } + + for key, val := range sr.Releases { + if len(val.Image.Repository) > 0 && val.Image.Repository == sr.Ctx.String("repository") && + val.Image.Tag != sr.Ctx.String("tag") { + val.Image.Tag = sr.Ctx.String("tag") + sr.Changes.List[path] = append(sr.Changes.List[path], key) + sr.Changes.Count++ + } + } + + sort.Strings(sr.Changes.List[path]) + + return nil +} + +func (sr *SpecRelease) serializeReleasesStruct() ([]byte, error) { + headComment := sr.NodeYAML.HeadComment + footComment := sr.NodeYAML.FootComment + + if err := sr.NodeYAML.Encode(&sr.Releases); err != nil { + return nil, err + } + + sr.NodeYAML.HeadComment = fmt.Sprintf("%s\r\n\n", headComment) + sr.NodeYAML.FootComment = footComment + + var data bytes.Buffer + + encoder := yaml.NewEncoder(&data) + encoder.SetIndent(2) + err := encoder.Encode(&sr.NodeYAML) + return data.Bytes(), err +} + +func (sr *SpecRelease) updateReleasesFile(g *git_handler.GitSpec) error { + if err := sr.searchReleasesPath(); err != nil { + return err + } + + if len(sr.ReleasesPaths) == 0 { + return fmt.Errorf("no files %s found", system.ReleasesFileName) + } + + sr.Changes.List = make(map[string][]string) + + for _, path := range sr.ReleasesPaths { + sr.Releases = make(map[string]*ReleaseStruct) + if err := sr.readReleasesFile(path); err != nil { + return err + } + + for key, val := range sr.Changes.List { + if key == path { + data, err := sr.serializeReleasesStruct() + if err != nil { + return err + } + + zap.S().Infof("tag changed for next releases %s, "+ + "affected file: %s", strings.Join(val, " "), path) + + if err := os.WriteFile(path, data, 0644); err != nil { + return err + } + } + } + } + + if sr.Changes.Count == 0 { + zap.S().Info("no image tag found to update by repositories URL") + return nil + } else if sr.Ctx.Bool("deploy") || sr.Ctx.Bool("commit") { + tmp := ¬ification.TmpUpdate{Config: sr.Conf, Context: sr.Ctx} + for key := range sr.Changes.List { + if err := g.GitCommitPush(key, sr.genMsgCommit(key), sr.Conf.GitHubToken); err != nil { + return err + } + + tmp.PathToFile = key + tmp.ChangesList = sr.Changes.List[key] + if err := notification.SlackInit(tmp, + notification.SlackTmp(tmp).TmpReleaseUpdateMsg()).SlackDeclareNotify(); err != nil { + return err + } + } + + if sr.Ctx.Bool("deploy") { + if errDep := sr.deployUpdatedReleases(); errDep != nil { + if err := notification.SlackInit(tmp, + notification.SlackTmp(tmp).TmpReleaseUpdateFailedMsg(errDep)).SlackFailNotify(); err != nil { + return err + } + + return errDep + } + + if err := notification.SlackInit(tmp, + notification.SlackTmp(tmp).TmpReleaseUpdateSuccessMsg()).SlackSuccessNotify(); err != nil { + return err + } + } + } + + return nil +} + +func (sr *SpecRelease) genMsgCommit(path string) string { + if sr.Ctx.Bool("skip-ci") { + return fmt.Sprintf("%s Auto version update %s for releases: %s", + "[skip ci]", + sr.Ctx.String("tag"), + strings.Join(sr.Changes.List[path], ",")) + } + + return fmt.Sprintf("Auto version update %s for releases: %s", + sr.Ctx.String("tag"), + strings.Join(sr.Changes.List[path], ",")) +} + +func (sr *SpecRelease) deployUpdatedReleases() error { + if err := sr.releaseMiddleware(); err != nil { + return err + } + + if err := sr.checkStatusRelease(); err != nil { + return err + } + + sr.SpecCMD = sr.prepareHelmfile() + for _, values := range sr.Changes.List { + for _, val := range values { + sr.SpecCMD.Args = append(sr.SpecCMD.Args, "-l", "app="+val) + } + } + + sr.SpecCMD.Args = append(sr.SpecCMD.Args, "sync") + + return sr.runCMD() +} + +func (rc *ReleaseCommands) helmCommands(args ...string) *system.SpecCMD { + return &system.SpecCMD{ + Args: args, + Command: "helm", + Ctx: rc.Ctx.Context, + Dir: rc.WorkDir, + Debug: true, + DisableStdOut: true, + Envs: append(rc.SpecCMD.Envs, + "AWS_PROFILE="+rc.Conf.Profile, + "AWS_CONFIG_FILE="+strings.Join(rc.Conf.AWSSharedConfigFile(rc.Conf.Profile), ""), + "AWS_SHARED_CREDENTIALS_FILE="+strings.Join(rc.Conf.AWSSharedCredentialsFile(rc.Conf.Profile), ""), + ), + } +} + +func (sr *SpecRelease) deserializeHelmStatus() *HelmStatus { + helmStatus := &HelmStatus{} + + regex, err := regexp.Compile("\n\n") + if err != nil { + zap.S().Fatal(err) + } + + if len(sr.SpecCMD.StdoutBuf.String()) == 0 { + return helmStatus + } + + if err := json.Unmarshal([]byte(regex.ReplaceAllString(sr.SpecCMD.StdoutBuf.String(), "\n")), &helmStatus); err != nil { + zap.S().Fatalf("can't deserialize Helm status output: %v", err) + } + + return helmStatus +} + +func (sr *SpecRelease) getNamespaceViaHelmfileList(releaseName string) (string, error) { + sr.SpecCMD = sr.prepareHelmfile("-l", "name="+releaseName, "list", "--output", "json") + sr.SpecCMD.DisableStdOut = true + sr.SpecCMD.Debug = true + if err := sr.runCMD(); err != nil { + return "", fmt.Errorf("Helmfile failed to get release %s namespace\n%s", releaseName, sr.SpecCMD.StderrBuf.String()) + } + + helmfileList := HelmfileList{} + + regex, err := regexp.Compile("\n\n") + if err != nil { + return "", err + } + + if len(sr.SpecCMD.StdoutBuf.String()) == 0 { + return "", nil + } + + if err := json.Unmarshal([]byte(regex.ReplaceAllString(sr.SpecCMD.StdoutBuf.String(), "\n")), &helmfileList); err != nil { + return "", fmt.Errorf("can't deserialize Helmfile list command output: %v", err) + } + + if len(helmfileList) > 0 { + return helmfileList[0].Namespace, nil + } else { + return "", fmt.Errorf("Helmfile return empty list for release: %s", releaseName) + } +} + +func (sr *SpecRelease) releaseStatus(releaseName string) error { + namespace, err := sr.getNamespaceViaHelmfileList(releaseName) + if err != nil { + return err + } + + sr.SpecCMD = sr.helmCommands("status", "--namespace", + namespace, + releaseName, + "--output", + "json", + ) + + if err := sr.runCMD(); err != nil { + return fmt.Errorf("Helm failed to get release %s status\n%s", releaseName, sr.SpecCMD.StderrBuf.String()) + } + + return nil +} + +func (sr *SpecRelease) releaseUnlock(status *HelmStatus) error { + sr.SpecCMD = sr.helmCommands("rollback", status.Name, + strconv.Itoa(status.Version), + "-n", + status.Namespace, + "--history-max", + "0", + ) + + if err := sr.runCMD(); err != nil { + return fmt.Errorf("Helm failed to rollback release: %s\n%s", status.Name, sr.SpecCMD.StderrBuf.String()) + } + + return nil +} + +func (sr *SpecRelease) checkStatusRelease() error { + var helmStatuses []HelmStatus + + for _, values := range sr.Changes.List { + for _, releaseName := range values { + if err := sr.releaseStatus(releaseName); err != nil { + return err + } + + status := sr.deserializeHelmStatus().Info.Status + + if status == "pending-upgrade" || status == "pending-install" { + helmStatuses = append(helmStatuses, *sr.deserializeHelmStatus()) + } + } + } + + for _, status := range helmStatuses { + if err := sr.releaseUnlock(&status); err != nil { + return err + } + + zap.S().Infof("unlock release %s for namespace %s was done", status.Name, status.Namespace) + } + + return nil +} + +func releaseHelmfileAction(conf *config.Config) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateArtifactModeDefault(c, ""); err != nil { + return err + } + + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { + return err + } + + rc := &ReleaseCommands{ + Conf: conf, + Ctx: c, + WorkDir: system.GetPwdPath(""), + } + + if !c.Bool("skip-context-switch") { + if err := rc.releaseKubeContext(); err != nil { + return err + } + } + + var args []string + + for _, selector := range c.StringSlice("selector") { + args = append(args, "-l", selector) + } + + args = append(args, c.Command.Name) + + if c.IsSet("output") { + args = append(args, "--output", c.String("output")) + } + + if c.IsSet("helmfile-args") { + // parse arguments using shell syntax (fully-compatible with any type of quotes) + shArgs, err := shell.Fields(c.String("helmfile-args"), func(name string) string { return "" }) + + if err != nil { + return fmt.Errorf("--helmfile-args argument has invalid shell syntax") + } + + args = append(args, shArgs...) + } + + return rc.releaseHelmfile(args...) + } +} + +func releaseRollbackAction(conf *config.Config) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateArtifactModeDefault(c, ""); err != nil { + return err + } + + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { + return err + } + + sr := &SpecRelease{ReleasesList: ReleasesList{Changes: struct { + List map[string][]string + Count int64 + }{List: make(map[string][]string)}}} + sr.Conf = conf + sr.Ctx = c + sr.WorkDir = system.GetPwdPath("") + + if !c.Bool("skip-context-switch") { + if err := sr.releaseKubeContext(); err != nil { + return err + } + } + + sr.Changes.List["rollback"] = c.StringSlice("release-name") + if err := sr.checkStatusRelease(); err != nil { + return err + } + + return nil + } +} + +func releaseUpdateAction(conf *config.Config, gitSpec *git_handler.GitSpec) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateArtifactModeDefault(c, ""); err != nil { + return err + } + + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { + return err + } + + sr := &SpecRelease{} + sr.Conf = conf + sr.Ctx = c + sr.WorkDir = system.GetPwdPath("") + + if !c.Bool("skip-context-switch") { + if err := sr.releaseKubeContext(); err != nil { + return err + } + } + + return sr.updateReleasesFile(gitSpec) + } +} diff --git a/commands/rmk_category.go b/commands/rmk_category.go new file mode 100644 index 0000000..a1269cb --- /dev/null +++ b/commands/rmk_category.go @@ -0,0 +1,89 @@ +package commands + +import ( + "fmt" + + "github.com/Masterminds/semver" + "github.com/urfave/cli/v2" + "go.uber.org/zap" + + "rmk/system" +) + +func completionAction() cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + fmt.Println(system.CompletionZshScript) + + return nil + } +} + +func docGenerateAction() cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + man, err := c.App.ToMarkdown() + if err != nil { + return nil + } + + fmt.Println(man) + + return nil + } +} + +func updateAction() cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + var version string + latestPath := "latest" + + if c.Bool("release-candidate") { + latestPath = "latest-rc" + } + + metadata, err := getRMKArtifactMetadata(latestPath) + if err != nil { + return err + } + + if len(c.String("version")) > 0 { + v, err := semver.NewVersion(c.String("version")) + if err != nil { + return err + } + + version = v.Original() + } + + verCurrent, _ := semver.NewVersion(fmt.Sprintf("%v", c.App.Metadata["version"])) + verFound, _ := semver.NewVersion(metadata.Version) + binaryName := fmt.Sprintf("%s", c.App.Metadata["binaryName"]) + if verCurrent.LessThan(verFound) && len(version) == 0 { + zap.S().Infof("newer release version RMK available: %s", verFound.Original()) + if err := updateRMK(binaryName, latestPath, false, true, c); err != nil { + return err + } + } else if len(version) > 0 { + zap.S().Infof("update current RMK version from %s to %s", + c.App.Metadata["version"], version) + if err := updateRMK(binaryName, version, false, true, c); err != nil { + return err + } + } else { + zap.S().Infof("installed RMK version %s is up-to-date", verCurrent.Original()) + } + + return nil + } +} diff --git a/commands/secret_category.go b/commands/secret_category.go new file mode 100644 index 0000000..c42385d --- /dev/null +++ b/commands/secret_category.go @@ -0,0 +1,461 @@ +package commands + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "strings" + "unicode" + + "github.com/urfave/cli/v2" + "go.uber.org/zap" + "gopkg.in/yaml.v3" + + "rmk/config" + "rmk/system" +) + +type SecretRunner interface { + helmSecretsEncrypt() error + helmSecretsDecrypt() error + helmSecretsView() error + helmSecretsEdit() error +} + +type SecretCommands struct { + *ReleaseCommands +} + +type SopsConfigFile struct { + CreationRules []CreationRule `yaml:"creation_rules"` +} + +type CreationRule struct { + PathRegex string `yaml:"path_regex"` + Age string `yaml:"age"` +} + +func newSecretCommands(conf *config.Config, ctx *cli.Context, workDir string) *SecretCommands { + return &SecretCommands{&ReleaseCommands{Conf: conf, Ctx: ctx, WorkDir: workDir}} +} + +func (sc *SecretCommands) ageKeygen(args ...string) *system.SpecCMD { + return &system.SpecCMD{ + Args: args, + Command: "age-keygen", + Ctx: sc.Ctx.Context, + Dir: sc.WorkDir, + Debug: false, + } +} + +func (sc *SecretCommands) helm() *system.SpecCMD { + return &system.SpecCMD{ + Args: []string{"secrets"}, + Command: "helm", + Ctx: sc.Ctx.Context, + Dir: sc.WorkDir, + Envs: []string{"SOPS_AGE_KEY_FILE=" + filepath.Join(sc.Conf.SopsAgeKeys, system.SopsAgeKeyFile)}, + Debug: true, + DisableStdOut: true, + } +} + +func (sc *SecretCommands) createAgeKey(scope string) error { + keyPath := filepath.Join(sc.Conf.SopsAgeKeys, sc.Conf.Tenant+"-"+scope+".txt") + + if system.IsExists(keyPath, true) { + return fmt.Errorf("key for scope %s exists, if you want to recreate, delete this file %s "+ + "and run the command again", scope, keyPath) + } + + sc.SpecCMD = sc.ageKeygen("-o", keyPath) + sc.SpecCMD.DisableStdOut = true + if err := runner(sc).runCMD(); err != nil { + return err + } + + sc.SpecCMD = sc.ageKeygen("-y", keyPath) + sc.SpecCMD.DisableStdOut = true + return runner(sc).runCMD() +} + +func (sc *SecretCommands) CreateKeys() error { + if !system.IsExists(system.GetPwdPath(system.TenantValuesDIR), false) { + return fmt.Errorf("'%s' directory not exist in project structure, please generate structure "+ + "by running command: 'rmk project generate'", system.TenantValuesDIR) + } + + scopes, err := os.ReadDir(system.GetPwdPath(system.TenantValuesDIR)) + if err != nil { + return err + } + + if err := os.MkdirAll(sc.Conf.SopsAgeKeys, 0775); err != nil { + return err + } + + for _, scope := range scopes { + if scope.IsDir() && !strings.Contains(scope.Name(), "cluster") { + if err := sc.createAgeKey(scope.Name()); err != nil { + return err + } + + zap.S().Infof("generate age key for scope: %s", scope.Name()) + + sopsConfigFiles, err := system.WalkInDir(system.GetPwdPath(system.TenantValuesDIR, scope.Name()), + "secrets", system.SopsConfigFile) + if err != nil { + return err + } + + if len(sopsConfigFiles) == 0 { + secretSpecFiles, err := system.WalkInDir(system.GetPwdPath(system.TenantValuesDIR, scope.Name()), + "secrets", system.SecretSpecFile) + if err != nil { + return err + } + + for _, specFile := range secretSpecFiles { + dirSpecFile, _ := filepath.Split(specFile) + sopsConfigFiles = append(sopsConfigFiles, filepath.Join(dirSpecFile, system.SopsConfigFile)) + } + } + + for _, configFile := range sopsConfigFiles { + sops := &SopsConfigFile{} + sops.CreationRules = append(sops.CreationRules, + CreationRule{ + PathRegex: ".+\\.yaml$", + Age: strings.ReplaceAll(sc.SpecCMD.StdoutBuf.String(), "\n", ""), + }) + + var data bytes.Buffer + encoder := yaml.NewEncoder(&data) + encoder.SetIndent(2) + if err := encoder.Encode(&sops); err != nil { + return err + } + + if err := os.WriteFile(configFile, data.Bytes(), 0644); err != nil { + return err + } + + zap.S().Infof("update SOPS config file: %s", configFile) + } + } + } + + return nil +} + +func (sc *SecretCommands) getOptionFiles(option string) ([]string, error) { + var optionFiles, optionPaths []string + var check int + + switch { + case !sc.Ctx.IsSet("scope") && !sc.Ctx.IsSet("environment"): + var err error + optionFiles, err = system.WalkInDir(system.GetPwdPath(system.TenantValuesDIR, filepath.Join(optionPaths...)), + "secrets", option) + if err != nil { + return nil, err + } + case sc.Ctx.IsSet("scope") && !sc.Ctx.IsSet("environment"): + for _, scope := range sc.Ctx.StringSlice("scope") { + sopsFiles, err := system.WalkInDir(system.GetPwdPath(system.TenantValuesDIR, scope), + "secrets", option) + if err != nil { + return nil, err + } + + optionFiles = append(optionFiles, sopsFiles...) + } + case !sc.Ctx.IsSet("scope") && sc.Ctx.IsSet("environment"): + for _, environment := range sc.Ctx.StringSlice("environment") { + for _, env := range sc.Conf.Project.Spec.Environments { + if environment == env { + check++ + } + } + + if check == 0 { + return nil, fmt.Errorf("environment %s do not exist in project.spec.environments", environment) + } + + sopsFiles, err := system.WalkInDir(system.GetPwdPath(system.TenantValuesDIR), + environment, filepath.Join("secrets", option)) + if err != nil { + return nil, err + } + + optionFiles = append(optionFiles, sopsFiles...) + } + case sc.Ctx.IsSet("scope") && sc.Ctx.IsSet("environment"): + for _, scope := range sc.Ctx.StringSlice("scope") { + for _, environment := range sc.Ctx.StringSlice("environment") { + optionPaths = append(optionPaths, scope, environment) + sopsFiles, err := system.WalkInDir(system.GetPwdPath(system.TenantValuesDIR, filepath.Join(optionPaths...)), + "secrets", option) + if err != nil { + return nil, err + } + + optionPaths = []string{} + optionFiles = append(optionFiles, sopsFiles...) + } + } + } + + return optionFiles, nil +} + +func (sc *SecretCommands) getSecretPaths(optionFiles []string) ([]string, error) { + var tempPaths, secretPaths []string + + for _, configFile := range optionFiles { + path := strings.ReplaceAll(configFile, string(filepath.Separator)+filepath.Base(configFile), "") + tempPaths = append(tempPaths, path) + } + + for _, tempPath := range tempPaths { + secrets, err := system.WalkMatch(tempPath, "*.yaml") + if err != nil { + return nil, err + } + + for _, secretPath := range secrets { + _, file := filepath.Split(secretPath) + if file != system.SopsConfigFile && file != system.SecretSpecFile { + secretPaths = append(secretPaths, secretPath) + } + } + } + + return secretPaths, nil +} + +func (sc *SecretCommands) SecretManager(option string) error { + optionFiles, err := sc.getOptionFiles(option) + if err != nil { + return err + } + + if sc.Ctx.Command.Name == "generate" { + if err := sc.genSpecSecrets(optionFiles); err != nil { + return err + } + + return nil + } + + secretPaths, err := sc.getSecretPaths(optionFiles) + if err != nil { + return err + } + + if err := system.MergeAgeKeys(sc.Conf.SopsAgeKeys); err != nil { + return err + } + + for _, secret := range secretPaths { + sc.SpecCMD = sc.helm() + + switch sc.Ctx.Command.Name { + case "decrypt": + sc.SpecCMD.Args = append(sc.SpecCMD.Args, sc.Ctx.Command.Name, "-i", secret) + if err := runner(sc).runCMD(); err != nil { + if strings.Contains(sc.SpecCMD.StderrBuf.String(), system.HelmSecretsIsNotEncrypted+secret) { + zap.S().Warnf(strings.ToLower(system.HelmSecretsIsNotEncrypted)+"%s", secret) + continue + } else { + return fmt.Errorf(sc.SpecCMD.StderrBuf.String()) + } + } + + zap.S().Infof("decrypting: %s", secret) + case "encrypt": + sc.SpecCMD.Args = append(sc.SpecCMD.Args, sc.Ctx.Command.Name, "-i", secret) + if err := runner(sc).runCMD(); err != nil { + if strings.Contains(sc.SpecCMD.StderrBuf.String(), system.HelmSecretsAlreadyEncrypted+filepath.Base(secret)) { + zap.S().Warnf(strings.ToLower(system.HelmSecretsAlreadyEncrypted)+"%s", secret) + continue + } else { + return fmt.Errorf(sc.SpecCMD.StderrBuf.String()) + } + } + + zap.S().Infof("encrypting: %s", secret) + } + } + + return nil +} + +func (sc *SecretCommands) helmSecretsEncrypt() error { + if err := system.MergeAgeKeys(sc.Conf.SopsAgeKeys); err != nil { + return err + } + + sc.SpecCMD = sc.helm() + sc.SpecCMD.Args = append(sc.SpecCMD.Args, sc.Ctx.Command.Name, "-i", sc.Ctx.Args().First()) + + return sc.runHelmSecretsCMD(sc.Ctx.Args().First(), false) +} + +func (sc *SecretCommands) helmSecretsDecrypt() error { + if err := system.MergeAgeKeys(sc.Conf.SopsAgeKeys); err != nil { + return err + } + + sc.SpecCMD = sc.helm() + sc.SpecCMD.Args = append(sc.SpecCMD.Args, sc.Ctx.Command.Name, "-i", sc.Ctx.Args().First()) + + return sc.runHelmSecretsCMD(sc.Ctx.Args().First(), false) +} + +func (sc *SecretCommands) helmSecretsView() error { + if err := system.MergeAgeKeys(sc.Conf.SopsAgeKeys); err != nil { + return err + } + + sc.SpecCMD = sc.helm() + sc.SpecCMD.Args = append(sc.SpecCMD.Args, "decrypt", sc.Ctx.Args().First()) + + if err := sc.runHelmSecretsCMD(sc.Ctx.Args().First(), false); err != nil { + return err + } + + fmt.Println(sc.SpecCMD.StdoutBuf.String()) + + return nil +} + +func (sc *SecretCommands) helmSecretsEdit() error { + if err := system.MergeAgeKeys(sc.Conf.SopsAgeKeys); err != nil { + return err + } + + sc.SpecCMD = sc.helm() + sc.SpecCMD.Args = append(sc.SpecCMD.Args, sc.Ctx.Command.Name, sc.Ctx.Args().First()) + sc.SpecCMD.DisableStdOut = false + + return sc.runHelmSecretsCMD(sc.Ctx.Args().First(), true) +} + +func (sc *SecretCommands) runHelmSecretsCMD(secretFilePath string, returnCMDError bool) error { + if !system.IsExists(secretFilePath, true) { + return fmt.Errorf("file does not exist: %s", secretFilePath) + } + + if err := runner(sc).runCMD(); err != nil { + if returnCMDError { + return err + } + + out := sc.SpecCMD.StderrBuf.String() + + // suppress help message of helm secrets + if strings.Contains(out, system.HelpFlagFull) { + return fmt.Errorf(system.UnknownErrorText, "Helm secrets") + } + + // remove unneeded text from helm secrets + out = strings.ReplaceAll(out, system.HelmSecretsOutputPrefix, "") + out = strings.ReplaceAll(out, system.HelmSecretsError, "") + out = strings.TrimSpace(out) + + // make the first letter lowercase + outRunes := []rune(out) + outRunes[0] = unicode.ToLower(outRunes[0]) + out = string(outRunes) + + return fmt.Errorf(out) + } + + return nil +} + +func secretMgrEncryptDecryptAction(conf *config.Config) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateArtifactModeDefault(c, ""); err != nil { + return err + } + + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { + return err + } + + return newSecretCommands(conf, c, system.GetPwdPath("")).SecretManager(system.SopsConfigFile) + } +} + +func secretMgrGenerateAction(conf *config.Config) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + return newSecretCommands(conf, c, system.GetPwdPath("")).SecretManager(system.SecretSpecFile) + } +} +func secretKeysCreateAction(conf *config.Config) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateArtifactModeDefault(c, ""); err != nil { + return err + } + + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { + return err + } + + return newSecretCommands(conf, c, system.GetPwdPath("")).CreateKeys() + } +} + +func secretKeysDownloadAction(conf *config.Config) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + return conf.DownloadFromBucket("", conf.SopsBucketName, conf.SopsAgeKeys, conf.Tenant) + } +} + +func secretKeysUploadAction(conf *config.Config) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateNArg(c, 0); err != nil { + return err + } + + return conf.UploadToBucket(conf.SopsBucketName, conf.SopsAgeKeys, "*"+system.SopsAgeKeyExt) + } +} + +func secretAction(conf *config.Config, action func(secretRunner SecretRunner) error) cli.ActionFunc { + return func(c *cli.Context) error { + if err := system.ValidateArtifactModeDefault(c, ""); err != nil { + return err + } + + if err := system.ValidateNArg(c, 1); err != nil { + return err + } + + if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { + return err + } + + return action(newSecretCommands(conf, c, system.GetPwdPath(""))) + } +} diff --git a/commands/secret_generation_category.go b/commands/secret_generation_category.go new file mode 100644 index 0000000..42c10fb --- /dev/null +++ b/commands/secret_generation_category.go @@ -0,0 +1,161 @@ +package commands + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "strings" + "text/template" + + "github.com/Masterminds/sprig/v3" + "go.uber.org/zap" + "golang.org/x/crypto/ssh/terminal" + "gopkg.in/yaml.v3" + + "rmk/system" +) + +// Custom name function for parsing template +const ( + Prompt = "prompt" + RequiredEnv = "requiredEnv" +) + +type GenerationSpec struct { + GenerationRules []GenerationRule `yaml:"generation-rules"` + secretsDir string +} + +type GenerationFuncMap struct { + preRender bool + funcMap template.FuncMap + tplString *bytes.Buffer +} + +type GenerationRule struct { + Name string `yaml:"name"` + Template string `yaml:"template"` +} + +func prompt(name string) (string, error) { + fmt.Printf("Enter %s: ", name) + passwd, err := terminal.ReadPassword(int(os.Stdin.Fd())) + fmt.Printf("\n") + if err != nil { + return "", err + } + + return string(passwd), nil +} + +func requiredEnv(name string) (string, error) { + if val, exists := os.LookupEnv(name); exists && len(val) > 0 { + return val, nil + } + + return "", fmt.Errorf("required env var %s is not set", name) +} + +func (gf *GenerationFuncMap) createFuncMap() { + gf.funcMap = sprig.TxtFuncMap() + for key, val := range map[string]interface{}{RequiredEnv: requiredEnv, Prompt: prompt} { + gf.funcMap[key] = val + } +} + +func (gf *GenerationFuncMap) newTemplate() *template.Template { + gf.createFuncMap() + tmpl := template.New("generate").Funcs(gf.funcMap) + if gf.preRender { + tmpl = tmpl.Option("missingkey=zero") + } else { + tmpl = tmpl.Option("missingkey=error") + } + + return tmpl +} + +func (gf *GenerationFuncMap) renderSpecTemplate(s string, data ...interface{}) error { + t, err := gf.newTemplate().Parse(s) + if err != nil { + return err + } + + var tplString bytes.Buffer + var d interface{} + if len(data) > 0 { + d = data[0] + } + + if err := t.Execute(&tplString, d); err != nil { + return err + } + + gf.tplString = &tplString + + return nil +} + +func (g *GenerationSpec) writeSpecSecrets(force bool) error { + for _, rule := range g.GenerationRules { + if system.IsExists(filepath.Join(g.secretsDir, rule.Name+".yaml"), true) && !force { + zap.S().Warnf("%s exists, new secret generation was skipped", + filepath.Join(g.secretsDir, rule.Name+".yaml")) + continue + } + + if strings.Contains(rule.Template, Prompt) || strings.Contains(rule.Template, RequiredEnv) { + genFunc := &GenerationFuncMap{preRender: false} + if err := genFunc.renderSpecTemplate(rule.Template); err != nil { + return err + } + + if err := os.WriteFile(filepath.Join(g.secretsDir, rule.Name+".yaml"), + genFunc.tplString.Bytes(), 0755); err != nil { + return err + } + + zap.S().Infof("generating: %s", filepath.Join(g.secretsDir, rule.Name+".yaml")) + + continue + } + + if err := os.WriteFile(filepath.Join(g.secretsDir, rule.Name+".yaml"), + []byte(rule.Template), 0755); err != nil { + return err + } + + zap.S().Infof("generating: %s", filepath.Join(g.secretsDir, rule.Name+".yaml")) + } + + return nil +} + +func (sc *SecretCommands) genSpecSecrets(specFiles []string) error { + genSpec := &GenerationSpec{} + + for _, spec := range specFiles { + data, err := os.ReadFile(spec) + if err != nil { + return err + } + + genSpec.secretsDir, _ = filepath.Split(spec) + + genFunc := &GenerationFuncMap{preRender: true} + if err := genFunc.renderSpecTemplate(string(data)); err != nil { + return err + } + + if err := yaml.Unmarshal(genFunc.tplString.Bytes(), &genSpec); err != nil { + return err + } + + if err := genSpec.writeSpecSecrets(sc.Ctx.Bool("force")); err != nil { + return err + } + } + + return nil +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..092f228 --- /dev/null +++ b/config/config.go @@ -0,0 +1,376 @@ +package config + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "text/template" + + "github.com/Masterminds/semver" + "github.com/urfave/cli/v2" + "go.uber.org/zap" + "gopkg.in/yaml.v3" + + "rmk/aws_provider" + "rmk/system" +) + +type Config struct { + Name string `yaml:"name,omitempty"` + Tenant string `yaml:"tenant,omitempty"` + Environment string `yaml:"environment,omitempty"` + ConfigFrom string `yaml:"config-from,omitempty"` + ArtifactMode string `yaml:"artifact-mode,omitempty"` + RootDomain string `yaml:"root-domain,omitempty"` + CloudflareToken string `yaml:"cloudflare-token,omitempty"` + GitHubToken string `yaml:"github-token,omitempty"` + S3ChartsRepoRegion string `yaml:"s3-charts-repo-region"` + ClusterProvider string `yaml:"cluster-provider"` + SlackNotifications bool `yaml:"slack-notifications"` + SlackWebHook string `yaml:"slack-webhook,omitempty"` + SlackChannel string `yaml:"slack-channel,omitempty"` + SlackMsgDetails []string `yaml:"slack-message-details,omitempty"` + SopsAgeKeys string `yaml:"sops-age-keys,omitempty"` + SopsBucketName string `yaml:"sops-bucket-name,omitempty"` + AWSECRHost string `yaml:"aws-ecr-host,omitempty"` + AWSECRRegion string `yaml:"aws-ecr-region,omitempty"` + AWSECRUserName string `yaml:"aws-ecr-user-name,omitempty"` + AWSMFAProfile string `yaml:"aws-mfa-profile,omitempty"` + AWSMFATokenExpiration string `yaml:"aws-mfa-token-expiration,omitempty"` + *aws_provider.AwsConfigure `yaml:"aws,omitempty"` + Terraform `yaml:"terraform,omitempty"` + ClusterProvisionerSL bool `yaml:"cluster-provisioner-state-locking"` + ExportedVars `yaml:"exported-vars,omitempty"` + ProgressBar bool `yaml:"progress-bar"` + ProjectFile `yaml:"project-file"` +} + +type ExportedVars struct { + TerraformOutput map[string]string `yaml:"terraform-output,omitempty"` + Env map[string]string `yaml:"env,omitempty"` +} + +type HookMapping struct { + Tenant string `yaml:"tenant,omitempty"` + Exists bool `yaml:"-"` + InheritedFrom string `yaml:"inherited-from,omitempty"` + *Package +} + +type Package struct { + Name string `yaml:"name,omitempty"` + Version string `yaml:"version,omitempty"` + Url string `yaml:"url,omitempty"` + Checksum string `yaml:"checksum,omitempty"` + Artifacts []string `yaml:"-"` + ArtifactUrl string `yaml:"artifact-url,omitempty"` + HelmfileTenant string `yaml:"-"` + OsLinux string `yaml:"os-linux,omitempty"` + OsMac string `yaml:"os-mac,omitempty"` + Os string `yaml:"-"` + Rename bool `yaml:"rename,omitempty"` + GitHubToken string `yaml:"-"` + DstPath string `yaml:"dst-path,omitempty"` +} + +type Inventory struct { + Clusters map[string]*Package `yaml:"clusters,omitempty"` + HelmPlugins map[string]*Package `yaml:"helm-plugins,omitempty"` + Hooks map[string]*Package `yaml:"hooks,omitempty"` + Tools map[string]*Package `yaml:"tools,omitempty"` +} + +type Project struct { + Dependencies []Package `yaml:"dependencies,omitempty"` + HooksMapping []HookMapping `yaml:"hooks-mapping,omitempty"` + Spec struct { + Environments []string `yaml:"environments,omitempty"` + Owners []string `yaml:"owners,omitempty"` + Scopes []string `yaml:"scopes,omitempty"` + } `yaml:"spec,omitempty"` +} + +type ProjectFile struct { + Project `yaml:"project,omitempty"` + Inventory `yaml:"inventory,omitempty"` +} + +type Terraform struct { + BucketName string `yaml:"bucket-name,omitempty"` + BucketKey string `yaml:"bucket-key,omitempty"` + DDBTableName string `yaml:"dynamodb-table-name,omitempty"` +} + +func (conf *Config) InitConfig(terraformOutput bool) *Config { + conf.ProjectFile = ProjectFile{} + if err := conf.ReadProjectFile(system.GetPwdPath(system.TenantProjectFile)); err != nil { + zap.S().Fatal(err) + } + + if !terraformOutput { + return conf + } + + conf.ExportedVars = ExportedVars{ + TerraformOutput: make(map[string]string), + Env: make(map[string]string), + } + + if err := conf.GetTerraformOutputs(); err != nil { + zap.S().Fatal(err) + } + + return conf +} + +func (conf *Config) SerializeConfig() ([]byte, error) { + var data bytes.Buffer + encoder := yaml.NewEncoder(&data) + encoder.SetIndent(2) + err := encoder.Encode(&conf) + return data.Bytes(), err +} + +func (conf *Config) SerializeJsonConfig() ([]byte, error) { + return json.Marshal(&conf) +} + +func (conf *Config) GetConfigs(all bool) error { + var tenantPattern string + configsPath := system.GetHomePath(system.RMKDir, system.RMKConfig) + + if all { + tenantPattern = "" + } else { + tenantPattern = conf.Tenant + } + + match, err := system.WalkMatch(configsPath, tenantPattern+"*.yaml") + if err != nil { + return err + } + + for _, val := range match { + fmt.Printf("- %s\n", strings.TrimSuffix(filepath.Base(val), filepath.Ext(filepath.Base(val)))) + } + + return nil +} + +func (conf *Config) SetRootDomain(c *cli.Context, gitSpecID string) error { + hostedZoneVar := system.TerraformVarsPrefix + system.TerraformVarHostedZoneName + if !c.IsSet("root-domain") { + if hostedZoneName, ok := conf.TerraformOutput[hostedZoneVar]; ok && len(hostedZoneName) > 0 { + if err := c.Set("root-domain", hostedZoneName); err != nil { + return err + } + } else { + if err := c.Set("root-domain", gitSpecID+system.TenantDomainSuffix); err != nil { + return err + } + } + } + + conf.RootDomain = c.String("root-domain") + + return nil +} + +func (conf *Config) GetTerraformOutputs() error { + type GetVar struct { + Type interface{} + Value interface{} + } + + var ( + raw map[string]*json.RawMessage + outputs map[string]*json.RawMessage + getVar *GetVar + ) + + checkWorkspace, err := conf.BucketKeyExists("", conf.Terraform.BucketName, "env:/"+conf.Name+"/tf.tfstate") + if err != nil { + return err + } + + if !checkWorkspace { + return nil + } + + data, err := conf.GetFileData(conf.Terraform.BucketName, "env:/"+conf.Name+"/tf.tfstate") + if err != nil { + return err + } + + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + if err := json.Unmarshal(*raw["outputs"], &outputs); err != nil { + return err + } + + if len(outputs) == 0 { + return nil + } + + for key := range outputs { + if strings.Contains(key, system.TerraformVarsPrefix) { + if err := json.Unmarshal(*outputs[key], &getVar); err != nil { + return err + } + + if reflect.TypeOf(getVar.Type).Kind() == reflect.String { + conf.TerraformOutput[key] = getVar.Value.(string) + conf.Env[strings.ToUpper(strings.ReplaceAll(key, system.TerraformVarsPrefix, ""))] = getVar.Value.(string) + } else { + zap.S().Warnf("Terraform output variable %s will not be exported as environment variable, "+ + "does not match the string type, current type: %s", key, getVar.Type) + } + } + } + + return nil +} + +func (pf *ProjectFile) ReadProjectFile(path string) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + + if err := yaml.Unmarshal(data, &pf); err != nil { + return err + } + + return pf.parseProjectFileData() +} + +func (pf *ProjectFile) ParseTemplate(tpl *template.Template, data interface{}, value string) (string, error) { + var text bytes.Buffer + defer text.Reset() + parse, err := tpl.Parse(value) + if err != nil { + return "", err + } + + err = parse.Execute(&text, &data) + if err != nil { + return "", err + } + + return text.String(), nil +} + +func (pf *ProjectFile) parseProjectFileData() error { + var err error + + for key, dep := range pf.Dependencies { + pf.Dependencies[key].Url, err = pf.ParseTemplate(template.New("Dependencies"), pf.Dependencies[key], dep.Url) + if err != nil { + return err + } + + if len(strings.Split(pf.Dependencies[key].Name, ".")) > 0 { + pf.Dependencies[key].HelmfileTenant = strings.Split(pf.Dependencies[key].Name, ".")[0] + } + + pf.Dependencies[key].ArtifactUrl, err = pf.ParseTemplate(template.New("Dependencies"), pf.Dependencies[key], dep.ArtifactUrl) + if err != nil { + return err + } + } + + for key, provider := range pf.Clusters { + if _, err := semver.NewVersion(provider.Version); err != nil { + return fmt.Errorf("%s %s for section inventory.clusters", strings.ToLower(err.Error()), provider.Version) + } + + pf.Clusters[key].Name = key + pf.Clusters[key].Url, err = pf.ParseTemplate(template.New("Clusters"), pf.Clusters[key], provider.Url) + if err != nil { + return err + } + } + + for key, plugin := range pf.HelmPlugins { + if _, err := semver.NewVersion(plugin.Version); err != nil { + return fmt.Errorf("%s %s for section inventory.helm-plugins", strings.ToLower(err.Error()), plugin.Version) + } + + pf.HelmPlugins[key].Name = key + pf.HelmPlugins[key].Url, err = pf.ParseTemplate(template.New("HelmPlugins"), pf.HelmPlugins[key], plugin.Url) + if err != nil { + return err + } + } + + for key, hook := range pf.Hooks { + if _, err := semver.NewVersion(hook.Version); err != nil { + return fmt.Errorf("%s %s for section inventory.hooks", strings.ToLower(err.Error()), hook.Version) + } + + pf.Hooks[key].Name = key + pf.Hooks[key].Url, err = pf.ParseTemplate(template.New("Hooks"), pf.Hooks[key], hook.Url) + if err != nil { + return err + } + } + + for key, tool := range pf.Tools { + if _, err := semver.NewVersion(tool.Version); err != nil { + return fmt.Errorf("%s %s for section inventory.tools", strings.ToLower(err.Error()), tool.Version) + } + + osDetect := runtime.GOOS + pf.Tools[key].Name = key + + switch osDetect { + case "darwin": + pf.Tools[key].Os = pf.Tools[key].OsMac + case "linux": + pf.Tools[key].Os = pf.Tools[key].OsLinux + default: + return fmt.Errorf("OS %s is not supported by RMK", osDetect) + } + + pf.Tools[key].Url, err = pf.ParseTemplate(template.New("Tools"), pf.Tools[key], tool.Url) + if err != nil { + return err + } + + pf.Tools[key].Checksum, err = pf.ParseTemplate(template.New("Tools"), pf.Tools[key], tool.Checksum) + if err != nil { + return err + } + } + + return nil +} + +func (conf *Config) ReadConfigFile(path string) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + + return yaml.Unmarshal(data, &conf) +} + +func (conf *Config) CreateConfigFile() error { + if err := os.MkdirAll(system.GetHomePath(system.RMKDir, system.RMKConfig), 0755); err != nil { + return err + } + + data, err := conf.SerializeConfig() + if err != nil { + return err + } + + return os.WriteFile(system.GetHomePath(system.RMKDir, system.RMKConfig, conf.Name+".yaml"), data, 0644) +} diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS new file mode 100644 index 0000000..8f2a70b --- /dev/null +++ b/docs/CODEOWNERS @@ -0,0 +1,3 @@ +# These owners will be the default owners for everything in +# the repo and will be requested for review when someone opens a pull request. +* @anovikov-el @apanasiuk-el diff --git a/git_handler/git.go b/git_handler/git.go new file mode 100644 index 0000000..d318a9c --- /dev/null +++ b/git_handler/git.go @@ -0,0 +1,308 @@ +package git_handler + +import ( + "context" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/transport" + "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/go-git/go-git/v5/plumbing/transport/ssh" + "go.uber.org/zap" + + "rmk/system" +) + +const ( + PrefixFeature = "feature/" + PrefixRelease = "release/" + DefaultDevelop = "develop" + DefaultStaging = "staging" + DefaultProduction = "production" + + TaskNum int = iota + SemVer +) + +type GitSpec struct { + DefaultBranches []string + DefaultBranch string + IntermediateBranch string + RepoName string + RepoPrefixName string + ID string + repo *git.Repository + auth transport.AuthMethod + workTree *git.Worktree + headRef *plumbing.Reference +} + +func (g *GitSpec) checkIntermediateBranchName(branch, prefix string) (int, error) { + g.IntermediateBranch = strings.ReplaceAll(branch, prefix, "") + patternTaskNum := regexp.MustCompile(`^[a-z]+-\d+`) + patternSemVer := regexp.MustCompile(`^v\d+\.\d+\.\d+(-[a-z]+)?$`) + + switch { + case len(patternTaskNum.FindString(strings.ToLower(g.IntermediateBranch))) > 0: + g.IntermediateBranch = patternTaskNum.FindString(strings.ToLower(g.IntermediateBranch)) + return TaskNum, nil + case len(patternSemVer.FindString(strings.ToLower(g.IntermediateBranch))) > 0: + g.IntermediateBranch = strings.ReplaceAll(patternSemVer.FindString(strings.ToLower(g.IntermediateBranch)), + ".", "-") + return SemVer, nil + default: + return 0, fmt.Errorf("selected branch %s cannot be used as environment name", branch) + } +} + +func (g *GitSpec) checkBranchName(branch string) error { + for _, val := range g.DefaultBranches { + if branch != val { + switch { + case strings.HasPrefix(branch, PrefixFeature): + if _, err := g.checkIntermediateBranchName(branch, PrefixFeature); err != nil { + return err + } + + g.DefaultBranch = DefaultDevelop + case strings.HasPrefix(branch, PrefixRelease): + pattern, err := g.checkIntermediateBranchName(branch, PrefixRelease) + if err != nil { + return err + } + + switch pattern { + case TaskNum: + g.DefaultBranch = DefaultStaging + case SemVer: + if strings.Contains(g.IntermediateBranch, "rc") { + g.DefaultBranch = DefaultStaging + break + } + + g.DefaultBranch = DefaultProduction + default: + return fmt.Errorf("selected branch %s cannot be used as environment name", branch) + } + } + } else { + g.DefaultBranch = val + } + } + + if len(g.DefaultBranch) == 0 && len(g.IntermediateBranch) == 0 { + return fmt.Errorf("selected branch %s cannot be used as environment name", branch) + } + + return nil +} + +func (g *GitSpec) GetBranchName() error { + openOptions := git.PlainOpenOptions{ + DetectDotGit: true, + } + + repo, err := git.PlainOpenWithOptions(system.GetPwdPath(""), &openOptions) + if err != nil { + return err + } + + head, err := repo.Head() + if err != nil { + return err + } + + if !head.Name().IsBranch() { + return fmt.Errorf("it's not branch %s", head.Name().Short()) + } + + return g.checkBranchName(head.Name().Short()) +} + +func (g *GitSpec) GetRepoPrefix() error { + openOptions := git.PlainOpenOptions{ + DetectDotGit: true, + } + + repo, err := git.PlainOpenWithOptions(system.GetPwdPath(""), &openOptions) + if err != nil { + return err + } + + c, err := repo.Config() + if err != nil { + return err + } + + if _, ok := c.Remotes["origin"]; !ok { + return fmt.Errorf("failed to extract prefix from repository name") + } else { + g.RepoName = strings.TrimSuffix(filepath.Base(strings.Join(c.Remotes["origin"].URLs, "")), ".git") + + if len(strings.Split(filepath.Base(g.RepoName), ".")) > 0 { + g.RepoPrefixName = strings.Split(filepath.Base(g.RepoName), ".")[0] + } + + return nil + } +} + +func (g *GitSpec) GenerateID() error { + if err := g.GetBranchName(); err != nil { + return err + } + + if err := g.GetRepoPrefix(); err != nil { + return err + } + + if len(g.IntermediateBranch) > 0 { + g.ID = g.RepoPrefixName + "-" + g.IntermediateBranch + } else { + g.ID = g.RepoPrefixName + "-" + g.DefaultBranch + } + + return nil +} + +func (g *GitSpec) GitCommitPush(pathRF, msg, token string) error { + var err error + + if g.repo, err = git.PlainOpen(system.GetPwdPath("")); err != nil { + return err + } + + if pathRF, err = filepath.Rel(system.GetPwdPath(""), pathRF); err != nil { + return err + } + + if g.workTree, err = g.repo.Worktree(); err != nil { + return err + } + + if _, err := g.workTree.Add(pathRF); err != nil { + return err + } + + // Commits the current staging area to the repository, with the new file + // just created. We should provide the object.Signature of Author of the + // commit Since version 5.0.1, we can omit the Author signature, being read + // from the git config files. + hash, err := g.workTree.Commit(msg, &git.CommitOptions{}) + if err != nil { + return err + } + + zap.S().Infof("hash commit - %s created with message: %s...", hash.String()[:7], strings.Split(msg, ",")[0]) + + if g.headRef, err = g.repo.Head(); err != nil { + return err + } + + if g.auth, err = g.getAuthMethod(token); err != nil { + return err + } + + reset := &system.SpecCMD{ + Args: []string{"reset", "--hard", "origin/" + g.headRef.Name().Short()}, + Command: "git", + Dir: system.GetPwdPath(""), + Ctx: context.TODO(), + DisableStdOut: true, + Debug: false, + } + + cherryPick := &system.SpecCMD{ + Args: []string{"cherry-pick", hash.String()}, + Command: "git", + Dir: system.GetPwdPath(""), + Ctx: context.TODO(), + DisableStdOut: true, + Debug: false, + } + + fetchOpt := &git.FetchOptions{ + RemoteName: "origin", + RefSpecs: []config.RefSpec{ + config.RefSpec( + fmt.Sprintf("+%s:refs/remotes/origin/%s", + g.headRef.Name(), + g.headRef.Name().Short(), + ), + ), + }, + Auth: g.auth, + Tags: git.NoTags, + Force: true, + } + + pushOpt := &git.PushOptions{ + RemoteName: "origin", + RefSpecs: []config.RefSpec{ + config.RefSpec( + fmt.Sprintf("%s:refs/heads/%s", + g.headRef.Name(), + g.headRef.Name().Short(), + ), + ), + }, + Auth: g.auth, + Progress: os.Stdout, + Force: false, + } + + if err = g.repo.Push(pushOpt); err != nil { + zap.S().Warnf("push operation: %s", err) + + zap.S().Infof("fetch remote origin: %s", g.headRef.Name().Short()) + if err := g.repo.Fetch(fetchOpt); err != nil { + return err + } + + zap.S().Infof("reset local branch %s by remote origin", g.headRef.Name().Short()) + if err := reset.ExecCMD(); err != nil { + return fmt.Errorf("Git failed to reset local branch %s\n%s", g.headRef.Name().Short(), + reset.StderrBuf.String()) + } + + zap.S().Infof("cherry-pick last hash commit %s", hash.String()[:7]) + if err := cherryPick.ExecCMD(); err != nil { + return fmt.Errorf("Git failed to cherry-pick last hash commit %s\n%s", hash.String()[:7], + cherryPick.StderrBuf.String()) + } + + if err := g.repo.Push(pushOpt); err != nil { + return err + } + } + + return nil +} + +func (g *GitSpec) getAuthMethod(token string) (transport.AuthMethod, error) { + c, err := g.repo.Config() + if err != nil { + return nil, err + } + + if _, ok := c.Remotes["origin"]; !ok { + return nil, fmt.Errorf("failed to detect auth method") + } else { + urls := c.Remotes["origin"].URLs + if len(urls) > 0 { + if strings.Contains(urls[0], "http") { + return &http.BasicAuth{Username: "git", Password: token}, nil + } + + return ssh.NewPublicKeysFromFile("git", system.GetHomePath(system.GitSSHPrivateKey), "") + } + + return nil, fmt.Errorf("failed to detect auth method") + } +} diff --git a/github/github.go b/github/github.go new file mode 100644 index 0000000..9bfb4ab --- /dev/null +++ b/github/github.go @@ -0,0 +1,76 @@ +package github + +import ( + "context" + "fmt" + "net/http" + "net/url" + + "github.com/google/go-github/github" + "golang.org/x/oauth2" +) + +// GitHub contains the functions necessary for interacting with GitHub release +// objects +type GitHub interface { + GetRelease(ctx context.Context, tag string) (*github.RepositoryRelease, error) +} + +// Client is the client for interacting with the GitHub API +type Client struct { + Owner, Repo string + *github.Client +} + +// NewClient creates and initializes a new GitHubClient +func NewClient(owner, repo, token, urlStr string) (GitHub, error) { + if len(owner) == 0 { + return nil, fmt.Errorf("missing GitHub repository owner") + } + + if len(repo) == 0 { + return nil, fmt.Errorf("missing GitHub repository name") + } + + baseURL, err := url.ParseRequestURI(urlStr) + if err != nil { + return nil, fmt.Errorf("failed to parse Github API URL: %v", err) + } + + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + tc := oauth2.NewClient(context.TODO(), ts) + + client := github.NewClient(tc) + client.BaseURL = baseURL + + return &Client{Owner: owner, Repo: repo, Client: client}, nil +} + +// GetRelease queries the GitHub API for a specified release object +func (c *Client) GetRelease(ctx context.Context, tag string) (*github.RepositoryRelease, error) { + // Check Release whether already exists or not + errHandler := func(release *github.RepositoryRelease, res *github.Response, err error) (*github.RepositoryRelease, error) { + if err != nil { + if res == nil { + return nil, fmt.Errorf("failed to get RMK release version: %s", tag) + } + + switch { + case res.StatusCode == http.StatusUnauthorized: + return nil, fmt.Errorf("wrong token is specified or there is no permission, invalid status: %s", res.Status) + case res.StatusCode != http.StatusNotFound: + return nil, fmt.Errorf("get RMK update release, invalid status: %s", res.Status) + } + + return nil, fmt.Errorf("RMK release version %s not found", tag) + } + + return release, nil + } + + if len(tag) == 0 { + return errHandler(c.Repositories.GetLatestRelease(ctx, c.Owner, c.Repo)) + } else { + return errHandler(c.Repositories.GetReleaseByTag(ctx, c.Owner, c.Repo, tag)) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d566e8b --- /dev/null +++ b/go.mod @@ -0,0 +1,113 @@ +module rmk + +go 1.21 + +require ( + github.com/Masterminds/semver v1.5.0 + github.com/Masterminds/sprig/v3 v3.2.3 + github.com/aws/aws-sdk-go-v2 v1.24.1 + github.com/aws/aws-sdk-go-v2/config v1.26.3 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.11 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.26.8 + github.com/aws/aws-sdk-go-v2/service/ecr v1.24.7 + github.com/aws/aws-sdk-go-v2/service/iam v1.28.7 + github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0 + github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 + github.com/aws/smithy-go v1.19.0 + github.com/cheggaaa/pb v1.0.29 + github.com/go-git/go-git/v5 v5.11.0 + github.com/google/go-github v17.0.0+incompatible + github.com/hashicorp/go-getter v1.7.3 + github.com/slack-go/slack v0.12.3 + github.com/urfave/cli/v2 v2.27.1 + go.uber.org/zap v1.26.0 + golang.org/x/crypto v0.18.0 + golang.org/x/oauth2 v0.16.0 + gopkg.in/yaml.v3 v3.0.1 + mvdan.cc/sh/v3 v3.8.0 +) + +require ( + cloud.google.com/go v0.110.2 // indirect + cloud.google.com/go/compute v1.20.1 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect + cloud.google.com/go/iam v0.13.0 // indirect + cloud.google.com/go/storage v1.29.0 // indirect + dario.cat/mergo v1.0.0 // indirect + github.com/BurntSushi/toml v1.3.2 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.2.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect + github.com/aws/aws-sdk-go v1.44.122 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.16.14 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.10 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.10 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.8.11 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.10 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.18.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6 // indirect + github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect + github.com/cloudflare/circl v1.3.3 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/s2a-go v0.1.4 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect + github.com/googleapis/gax-go/v2 v2.11.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-safetemp v1.0.0 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + github.com/huandu/xstrings v1.3.3 // indirect + github.com/imdario/mergo v0.3.11 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/compress v1.15.11 // indirect + github.com/mattn/go-runewidth v0.0.4 // indirect + github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/mitchellh/reflectwalk v1.0.0 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sergi/go-diff v1.1.0 // indirect + github.com/shopspring/decimal v1.2.0 // indirect + github.com/skeema/knownhosts v1.2.1 // indirect + github.com/spf13/cast v1.3.1 // indirect + github.com/ulikunitz/xz v0.5.10 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + go.opencensus.io v0.24.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/term v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.17.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + google.golang.org/api v0.126.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect + google.golang.org/grpc v1.55.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6474851 --- /dev/null +++ b/go.sum @@ -0,0 +1,1117 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= +cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA= +cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= +cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg= +cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= +cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k= +cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= +cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +cloud.google.com/go/storage v1.29.0 h1:6weCgzRvMg7lzuUurI4697AqIRPU1SvzHhynwpW31jI= +cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= +github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aws/aws-sdk-go v1.44.122 h1:p6mw01WBaNpbdP2xrisz5tIkcNwzj/HysobNoaAHjgo= +github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU= +github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo= +github.com/aws/aws-sdk-go-v2/config v1.26.3 h1:dKuc2jdp10y13dEEvPqWxqLoc0vF3Z9FC45MvuQSxOA= +github.com/aws/aws-sdk-go-v2/config v1.26.3/go.mod h1:Bxgi+DeeswYofcYO0XyGClwlrq3DZEXli0kLf4hkGA0= +github.com/aws/aws-sdk-go-v2/credentials v1.16.14 h1:mMDTwwYO9A0/JbOCOG7EOZHtYM+o7OfGWfu0toa23VE= +github.com/aws/aws-sdk-go-v2/credentials v1.16.14/go.mod h1:cniAUh3ErQPHtCQGPT5ouvSAQ0od8caTO9OOuufZOAE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.11 h1:I6lAa3wBWfCz/cKkOpAcumsETRkFAl70sWi8ItcMEsM= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.11/go.mod h1:be1NIO30kJA23ORBLqPo1LttEM6tPNSEcjkd1eKzNW0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.10 h1:5oE2WzJE56/mVveuDZPJESKlg/00AaS2pY2QZcnxg4M= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.10/go.mod h1:FHbKWQtRBYUz4vO5WBWjzMD2by126ny5y/1EoaWoLfI= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.26.8 h1:XKO0BswTDeZMLDBd/b5pCEZGttNXrzRUVtFvp2Ak/Vo= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.26.8/go.mod h1:N5tqZcYMM0N1PN7UQYJNWuGyO886OfnMhf/3MAbqMcI= +github.com/aws/aws-sdk-go-v2/service/ecr v1.24.7 h1:3iaT/LnGV6jNtbBkvHZDlzz7Ky3wMHDJAyFtGd5GUJI= +github.com/aws/aws-sdk-go-v2/service/ecr v1.24.7/go.mod h1:mtzCLxk6M+KZbkJdq3cUH9GCrudw8qCy5C3EHO+5vLc= +github.com/aws/aws-sdk-go-v2/service/iam v1.28.7 h1:FKPRDYZOO0Eur19vWUL1B40Op0j89KQj3kARjrszMK8= +github.com/aws/aws-sdk-go-v2/service/iam v1.28.7/go.mod h1:YzMYyQ7S4twfYzLjwP24G1RAxypozVZeNaG1r2jxRms= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.10 h1:L0ai8WICYHozIKK+OtPzVJBugL7culcuM4E4JOpIEm8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.10/go.mod h1:byqfyxJBshFk0fF9YmK0M0ugIO8OWjzH2T3bPG4eGuA= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.8.11 h1:e9AVb17H4x5FTE5KWIP5M1Du+9M86pS+Hw0lBUdN8EY= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.8.11/go.mod h1:B90ZQJa36xo0ph9HsoteI1+r8owgQH/U1QNfqZQkj1Q= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.10 h1:KOxnQeWy5sXyS37fdKEvAsGHOr9fa/qvwxfJurR/BzE= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.10/go.mod h1:jMx5INQFYFYB3lQD9W0D8Ohgq6Wnl7NYOJ2TQndbulI= +github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0 h1:PJTdBMsyvra6FtED7JZtDpQrIAflYDHFoZAu/sKYkwU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0/go.mod h1:4qXHrG1Ne3VGIMZPCB8OjH/pLFO94sKABIusjh0KWPU= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.6 h1:dGrs+Q/WzhsiUKh82SfTVN66QzyulXuMDTV/G8ZxOac= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.6/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6 h1:Yf2MIo9x+0tyv76GljxzqA3WtC5mw7NmazD2chwjxE4= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U= +github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= +github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= +github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo= +github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +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/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= +github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= +github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= +github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +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/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +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.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +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.4.1/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.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/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.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= +github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= +github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= +github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= +github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4= +github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-getter v1.7.3 h1:bN2+Fw9XPFvOCjB0UOevFIMICZ7G2XSQHzfvLUyOM5E= +github.com/hashicorp/go-getter v1.7.3/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744= +github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= +github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/slack-go/slack v0.12.3 h1:92/dfFU8Q5XP6Wp5rr5/T5JHLM5c5Smtn53fhToAP88= +github.com/slack-go/slack v0.12.3/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= +github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +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-20190108225652-1e06a53dbb7e/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-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +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-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +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-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= +google.golang.org/api v0.126.0 h1:q4GJq+cAdMAC7XP7njvQ4tvohGLiSlytuL4BQxbIZ+o= +google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= +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/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= +google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao= +google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +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.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= +google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +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.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +mvdan.cc/sh/v3 v3.8.0 h1:ZxuJipLZwr/HLbASonmXtcvvC9HXY9d2lXZHnKGjFc8= +mvdan.cc/sh/v3 v3.8.0/go.mod h1:w04623xkgBVo7/IUK89E0g8hBykgEpN0vgOj3RJr6MY= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/go_getter/getter.go b/go_getter/getter.go new file mode 100644 index 0000000..3e90261 --- /dev/null +++ b/go_getter/getter.go @@ -0,0 +1,144 @@ +package go_getter + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "os/signal" + "sync" + + "github.com/cheggaaa/pb" + "github.com/hashicorp/go-getter" + "go.uber.org/zap" +) + +type ProgressBar struct { + lock sync.Mutex + pool *pb.Pool + pbs int +} + +var defaultProgressBar getter.ProgressTracker = &ProgressBar{} + +func ProgressBarConfig(bar *pb.ProgressBar) { + bar.SetUnits(pb.U_BYTES) +} + +func (cpb *ProgressBar) TrackProgress(src string, currentSize, totalSize int64, stream io.ReadCloser) io.ReadCloser { + cpb.lock.Lock() + defer cpb.lock.Unlock() + + emptyReturn := &readCloser{Reader: stream, close: func() error { return nil }} + + if totalSize <= 0 { + return emptyReturn + } + + newPb := pb.New64(totalSize) + newPb.Set64(currentSize) + ProgressBarConfig(newPb) + if cpb.pool == nil { + cpb.pool = pb.NewPool() + err := cpb.pool.Start() + if err != nil { + return emptyReturn + } + } + + cpb.pool.Add(newPb) + reader := newPb.NewProxyReader(stream) + cpb.pbs++ + + return &readCloser{ + Reader: reader, + close: func() error { + cpb.lock.Lock() + defer cpb.lock.Unlock() + newPb.Finish() + cpb.pbs-- + if cpb.pbs == 0 { + err := cpb.pool.Stop() + if err != nil { + return err + } + cpb.pool = nil + } + return nil + }, + } +} + +type readCloser struct { + io.Reader + close func() error +} + +func (c *readCloser) Close() error { + return c.close() +} + +func DownloadArtifact(src, dst, name string, header *http.Header, silent, progress bool, ctxP context.Context) error { + ctx, cancel := context.WithCancel(ctxP) + + client := &getter.Client{ + Ctx: ctx, + //define the destination to where the directory will be stored. This will create the directory if it doesnt exist + Dst: dst, + Dir: true, + //the repository with a subdirectory I would like to clone only + Src: src, + Mode: getter.ClientModeAny, + //define the type of detectors go getter should use, in this case only github is needed + //Detectors: []getter.Detector{ + // &getter.GitHubDetector{}, + //}, + //provide the getter needed to download the files + Getters: map[string]getter.Getter{ + "file": new(getter.FileGetter), + "git": new(getter.GitGetter), + "gcs": new(getter.GCSGetter), + "hg": new(getter.HgGetter), + "s3": new(getter.S3Getter), + "http": &getter.HttpGetter{Header: *header}, + "https": &getter.HttpGetter{Header: *header}, + }, + } + + if progress && !silent { + client.ProgressListener = defaultProgressBar + } + + wg := sync.WaitGroup{} + wg.Add(1) + errChan := make(chan error, 2) + go func() { + defer wg.Done() + defer cancel() + if err := client.Get(); err != nil { + errChan <- err + } + }() + + c := make(chan os.Signal) + signal.Notify(c, os.Interrupt) + + select { + case sig := <-c: + signal.Reset(os.Interrupt) + cancel() + wg.Wait() + return fmt.Errorf("signal: %v", sig) + case <-ctx.Done(): + wg.Wait() + if !silent { + zap.S().Infof("downloaded: %s", name) + } + case err := <-errChan: + wg.Wait() + return fmt.Errorf("error downloading: %s", err) + } + + return nil +} diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..4779984 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,113 @@ +package logger + +import ( + "fmt" + "os" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type Logger struct { + encoder zapcore.Encoder + stdoutSyncer zapcore.WriteSyncer + stderrSyncer zapcore.WriteSyncer + cores []zapcore.Core + Format string + Level string +} + +func debugLevel() zap.LevelEnablerFunc { + return func(level zapcore.Level) bool { + return level == zapcore.DebugLevel + } +} + +func infoLevel() zap.LevelEnablerFunc { + return func(level zapcore.Level) bool { + return level == zapcore.InfoLevel || level == zapcore.WarnLevel + } +} + +func errorLevel() zap.LevelEnablerFunc { + return func(level zapcore.Level) bool { + return level == zapcore.ErrorLevel || level == zapcore.FatalLevel + } +} + +func (l *Logger) debugCores() []zapcore.Core { + return append(l.cores, + zapcore.NewCore(l.encoder, l.stdoutSyncer, debugLevel()), + zapcore.NewCore(l.encoder, l.stdoutSyncer, infoLevel()), + zapcore.NewCore(l.encoder, l.stderrSyncer, errorLevel()), + ) +} + +func (l *Logger) infoCores() []zapcore.Core { + return append(l.cores, + zapcore.NewCore(l.encoder, l.stdoutSyncer, infoLevel()), + zapcore.NewCore(l.encoder, l.stderrSyncer, errorLevel()), + ) +} + +func (l *Logger) errorCores() []zapcore.Core { + return append(l.cores, zapcore.NewCore(l.encoder, l.stderrSyncer, errorLevel())) +} + +func Init(format, level string) func() { + var logError error + + l := &Logger{ + stderrSyncer: zapcore.Lock(os.Stderr), + stdoutSyncer: zapcore.Lock(os.Stdout), + Format: format, + Level: level, + } + + cfgConsole := zapcore.EncoderConfig{ + MessageKey: "message", + LevelKey: "level", + TimeKey: "time", + //CallerKey: "caller", + EncodeLevel: zapcore.CapitalColorLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + //EncodeCaller: zapcore.ShortCallerEncoder, + } + + switch l.Format { + case "json": + cfgJson := cfgConsole + cfgJson.EncodeLevel = zapcore.LowercaseLevelEncoder + l.encoder = zapcore.NewJSONEncoder(cfgJson) + case "console": + l.encoder = zapcore.NewConsoleEncoder(cfgConsole) + default: + l.encoder = zapcore.NewConsoleEncoder(cfgConsole) + logError = fmt.Errorf("invalid log output format %s, available: console, json", l.Format) + } + + switch l.Level { + case "debug": + l.cores = l.debugCores() + case "info", "warning": + l.cores = l.infoCores() + case "error": + l.cores = l.errorCores() + default: + l.cores = l.errorCores() + logError = fmt.Errorf("invalid log level %s, available: debug, info, error", l.Level) + } + + // tee core + core := zapcore.NewTee(l.cores...) + + // finally construct the logger with the tee core + logger := zap.New(core, zap.AddCaller()) + undo := zap.ReplaceGlobals(logger) + + if logError != nil { + zap.S().Fatal(logError) + } + + return undo +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..0335397 --- /dev/null +++ b/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "fmt" + "os" + "sort" + + "github.com/urfave/cli/v2" + "go.uber.org/zap" + + "rmk/commands" + "rmk/logger" +) + +var ( + builtBy string + commit string + date string + name string + target string + timestamp string + version string +) + +// Override default help template parsing with add [GLOBAL OPTIONS] for all commands and subcommands +func init() { + var output string + + for _, val := range commands.FlagsGlobal() { + output += fmt.Sprintf(" %s\n", val.String()) + } + + cli.CommandHelpTemplate += ` +GLOBAL OPTIONS: +` + output + `{{range $index, $option := .VisibleFlags}}{{if eq $option.Name "help"}}{{" "}}{{$option}}{{end}}{{end}} +` + + cli.SubcommandHelpTemplate += ` +GLOBAL OPTIONS: +` + output + `{{range $index, $option := .VisibleFlags}}{{if eq $option.Name "help"}}{{" "}}{{$option}}{{end}}{{end}} +` +} + +func runCLI() *cli.App { + app := cli.NewApp() + app.Name = name + app.Description = "Command line tool for reduced management of the " + + "provision of Kubernetes clusters in different environments and management of service releases." + + fmt.Sprintf("\nBuiltBy: %s\nCommit: %s\nDate: %s\nTarget: %s", + builtBy, commit, date, target) + app.Usage = "Reduced management for Kubernetes" + app.Version = version + app.Metadata = map[string]interface{}{ + "binaryName": name + "_" + target, + "timestamp": timestamp, + "version": version, + } + + app.Flags = commands.FlagsGlobal() + app.Before = func(c *cli.Context) error { + logger.Init(c.String("log-format"), c.String("log-level")) + return nil + } + + // Enable command auto-completion (the --generate-bash-completion flag is provided out of box) + // Incompatible with UseShortOptionHandling option + app.EnableBashCompletion = true + // Enable flag and command suggestions + app.Suggest = true + + app.Commands = commands.Commands() + sort.Sort(cli.CommandsByName(app.Commands)) + + return app +} + +func main() { + err := runCLI().Run(os.Args) + if err != nil { + zap.S().Fatal(err) + } +} diff --git a/notification/slack.go b/notification/slack.go new file mode 100644 index 0000000..d1ea947 --- /dev/null +++ b/notification/slack.go @@ -0,0 +1,62 @@ +package notification + +import ( + "fmt" + + "github.com/slack-go/slack" + "go.uber.org/zap" +) + +const ( + DeclaredIconURL = "https://img.icons8.com/pastel-glyph/64/26e07f/point-to-beginning.png" + SuccessIconURL = "https://img.icons8.com/ios-glyphs/60/26e07f/rocket.png" + FailedIconURL = "https://img.icons8.com/external-tal-revivo-color-tal-revivo/96/fa314a/external-fatal-error-notification-in-computer-operating-system-basic-color-tal-revivo.png" +) + +type SlackConfig struct { + *TmpUpdate + *slack.WebhookMessage +} + +func SlackInit(tmp *TmpUpdate, msg string) *SlackConfig { + return &SlackConfig{tmp, &slack.WebhookMessage{ + Channel: tmp.SlackChannel, + Text: msg, + }} +} + +func (s *SlackConfig) SlackDeclareNotify() error { + s.Username = s.Command.HelpName + ": declared" + s.IconURL = DeclaredIconURL + return s.slackPostMsg("declared") +} + +func (s *SlackConfig) SlackSuccessNotify() error { + s.Username = s.Command.HelpName + ": success" + s.IconURL = SuccessIconURL + return s.slackPostMsg("success") +} + +func (s *SlackConfig) SlackFailNotify() error { + s.Username = s.Command.HelpName + ": failed" + s.IconURL = FailedIconURL + return s.slackPostMsg("failed") +} + +func (s *SlackConfig) slackPostMsg(status string) error { + if !s.SlackNotifications { + return nil + } else { + if len(s.SlackWebHook) == 0 || len(s.SlackChannel) == 0 { + zap.S().Fatalf("parameters --slack-webhook, --slack-channel not set for command " + + "'rmk config init', required if Slack notifications are enabled") + } + } + + zap.S().Infof("sending message %s to Slack channel: %s", status, s.Channel) + if err := slack.PostWebhook(s.SlackWebHook, s.WebhookMessage); err != nil { + return fmt.Errorf("failed to notify to Slack: %w", err) + } + + return nil +} diff --git a/notification/slack_templates.go b/notification/slack_templates.go new file mode 100644 index 0000000..2e569c3 --- /dev/null +++ b/notification/slack_templates.go @@ -0,0 +1,82 @@ +package notification + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/urfave/cli/v2" + + "rmk/config" +) + +type SlackTmp interface { + TmpProjectUpdateMsg() string + TmpReleaseUpdateMsg() string + TmpReleaseUpdateSuccessMsg() string + TmpReleaseUpdateFailedMsg(err error) string + TmpUpdateMsgDetails() string +} + +type TmpUpdate struct { + ChangesList []string + PathToFile string + *cli.Context + *config.Config +} + +func (t *TmpUpdate) TmpProjectUpdateMsg() string { + return fmt.Sprintf("*Dependency:* _%s_\n"+ + "*New version:* `%s`\n"+ + "\t*Affected tenant:* %s\n"+ + "\t*Affected environment:* %s\n"+ + "\t*Affected file:* %s\n", + strings.Join(t.ChangesList, " "), + t.Context.String("version"), + t.Tenant, + t.Environment, + t.PathToFile, + ) + t.TmpUpdateMsgDetails() +} + +func (t *TmpUpdate) TmpReleaseUpdateSuccessMsg() string { + return fmt.Sprintf("*Success deployed releases:* _%s_\n"+ + "*For cluster:* %s\n", + strings.Join(t.ChangesList, " "), + t.RootDomain, + ) + t.TmpUpdateMsgDetails() +} + +func (t *TmpUpdate) TmpReleaseUpdateFailedMsg(err error) string { + return fmt.Sprintf("*Fail deployed releases:* _%s_\n"+ + "*For cluster:* %s\n"+ + "*Error:* %s\n", + strings.Join(t.ChangesList, " "), + t.RootDomain, + err, + ) + t.TmpUpdateMsgDetails() +} + +func (t *TmpUpdate) TmpReleaseUpdateMsg() string { + scope := strings.Split(t.PathToFile, string(filepath.Separator)) + + return fmt.Sprintf("*Releases:* _%s_\n"+ + "*New version:* `%s`\n"+ + "\t*Affected tenant:* %s\n"+ + "\t*Affected environment:* %s\n"+ + "\t*Affected scope:* %s\n", + strings.Join(t.ChangesList, " "), + t.Context.String("tag"), + t.Tenant, + t.Environment, + scope[len(scope)-3], + ) + t.TmpUpdateMsgDetails() +} + +func (t *TmpUpdate) TmpUpdateMsgDetails() string { + if len(t.SlackMsgDetails) > 0 { + return fmt.Sprintf("*Details:*\n\t- %s", strings.Join(t.SlackMsgDetails, "\n\t- ")) + } + + return "" +} diff --git a/system/dictionary.go b/system/dictionary.go new file mode 100644 index 0000000..b33e428 --- /dev/null +++ b/system/dictionary.go @@ -0,0 +1,90 @@ +package system + +const ( + AWSClusterProvider = "aws" + AWSECRHost = "288509344804.dkr.ecr.eu-north-1.amazonaws.com" + AWSECRRegion = "eu-north-1" + AWSECRUserName = "AWS" + ArtifactDownloadDir = "dist" + ArtifactModeDefault = "none" + ArtifactModeOffline = "offline" + ArtifactModeOnline = "online" + GitSSHPrivateKey = ".ssh/id_rsa" + GlobalsFileName = "globals.yaml.gotmpl" + HelmfileFileName = "helmfile.yaml" + HelmfileGoTmplName = HelmfileFileName + ".gotmpl" + HelpFlagFull = "--help" + K3DConfigPrefix = "k3d" + RMKBin = "rmk" + RMKBucketName = "edenlabllc-rmk" + RMKBucketRegion = "eu-north-1" + RMKConfig = "config" + RMKDir = ".rmk" + RMKSymLinkPath = "/usr/local/bin/rmk" + RMKToolsDir = "tools" + ReadmeFileName = "README.md" + RegionException = "us-east-1" + ReleasesFileName = "releases.yaml" + S3ChartsRepoRegion = "eu-north-1" + SecretSpecFile = ".spec.yaml.gotmpl" + SopsAgeKeyExt = ".txt" + SopsAgeKeyFile = ".keys.txt" + SopsRootName = "sops-age-keys" + SopsConfigFile = ".sops.yaml" + TenantBucketKey = "tf.tfstate" + TenantBucketName = "cloud-provisioner" + TenantDDBTablePrefix = "cloud-provisioner-state-locks" + TenantDomainSuffix = ".edenlab.dev" + TenantProjectCodeOwners = "docs/CODEOWNERS" + TenantProjectDIR = ".PROJECT" + TenantProjectFile = "project.yaml" + TenantProjectGitIgn = ".gitignore" + TenantValuesDIR = "etc" + TerraformVarHostedZoneName = "hosted_zone_name" + TerraformVarsExt = "tfvars" + TerraformVarsFile = "variables.auto." + TerraformVarsExt + TerraformVarsPrefix = "rmk_" + TerraformWGFile = "worker-groups.auto." + TerraformVarsExt + ToolsBinDir = "bin" + ToolsTmpDir = "tmp" + ToolsVersionDir = "version" + + ConfigNotInitializedErrorText = "RMK config not initialized, " + + "please run command 'rmk config init' with specific parameters" + // UnknownErrorText standard text for unknown errors + UnknownErrorText = "unknown error when calling %s" + //HelmPluginExist HelmSecretsIsNotEncrypted HelmSecretsAlreadyEncrypted - exception err text matching + HelmPluginExist = "Error: plugin already exists" + HelmSecretsIsNotEncrypted = "File is not encrypted: " + HelmSecretsAlreadyEncrypted = "Already encrypted: " + HelmSecretsOutputPrefix = "[helm-secrets] " + HelmSecretsError = "Error: plugin \"secrets\" exited with error" + + CompletionZshDescription = `Run the following scripts to enable Zsh completion: + +rmk completion zsh > ~/.local/bin/rmk-completion-zsh.sh +chmod +x ~/.local/bin/rmk-completion-zsh.sh +echo "PROG=rmk source ~/.local/bin/rmk-completion-zsh.sh" >> ~/.zshrc` + + // CompletionZshScript https://github.com/urfave/cli/blob/v2.27.1/autocomplete/zsh_autocomplete + CompletionZshScript = `#compdef $PROG + +_cli_zsh_autocomplete() { + local -a opts + local cur + cur=${words[-1]} + if [[ "$cur" == "-"* ]]; then + opts=("${(@f)$(${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}") + else + opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-bash-completion)}") + fi + + if [[ "${opts[1]}" != "" ]]; then + _describe 'values' opts + else + _files + fi +} + +compdef _cli_zsh_autocomplete $PROG` +) diff --git a/system/system.go b/system/system.go new file mode 100644 index 0000000..e91f73f --- /dev/null +++ b/system/system.go @@ -0,0 +1,494 @@ +package system + +import ( + "archive/tar" + "bufio" + "bytes" + "compress/gzip" + "context" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "sync" + "unicode/utf8" + + "github.com/urfave/cli/v2" + "github.com/urfave/cli/v2/altsrc" + "go.uber.org/zap" +) + +type SpecCMD struct { + Args []string + Envs []string + Command string + Dir string + Ctx context.Context + StdoutBuf bytes.Buffer + StderrBuf bytes.Buffer + CommandStr string + DisableStdOut bool + Debug bool + SensKeyWords []string +} + +func (s *SpecCMD) AddEnv() error { + path, exists := os.LookupEnv("PATH") + if exists { + if err := os.Setenv("PATH", GetHomePath(".local", "bin")+":"+path); err != nil { + return err + } + } + + s.Envs = append(os.Environ(), s.Envs...) + + return nil +} + +func (s *SpecCMD) sensitive(data []byte) ([]byte, error) { + for _, word := range s.SensKeyWords { + regex, err := regexp.Compile(word) + if err != nil { + return nil, err + } + + data = regex.ReplaceAllLiteral(data, []byte("[rmk_sensitive]")) + } + + return data, nil +} + +func (s *SpecCMD) copyAndCapture(r io.Reader, w ...io.Writer) error { + var errSens error + buf := make([]byte, 1024) + + for { + n, err := r.Read(buf[:]) + if n > 0 { + data := buf[:n] + data, errSens = s.sensitive(data) + if errSens != nil { + return errSens + } + + for _, val := range w { + if _, err := val.Write(data); err != nil { + return err + } + } + } + + if err != nil { + // Read returns io.EOF at the end of file, which is not an error for us + if err == io.EOF { + err = nil + } + + return err + } + } +} + +func (s *SpecCMD) disableStdOut(w ...io.Writer) []io.Writer { + if s.DisableStdOut { + return w[:1] + } + + return w +} + +func (s *SpecCMD) ExecCMD() error { + var ( + wg sync.WaitGroup + stdoutIn, stderrIn io.ReadCloser + err error + ) + + cmd := exec.CommandContext(s.Ctx, s.Command, s.Args...) + cmd.Dir = s.Dir + cmd.Env = s.Envs + if stdoutIn, err = cmd.StdoutPipe(); err != nil { + return err + } + + if stderrIn, err = cmd.StderrPipe(); err != nil { + return err + } + + cmd.Stdin = os.Stdout + + s.CommandStr = cmd.String() + + err = cmd.Start() + if err != nil { + return err + } + + // cmd.Wait() should be called only after we finish reading + // from stdoutIn and stderrIn. + // wg ensures that we finish + wg.Add(1) + go func() { + if err = s.copyAndCapture(stdoutIn, s.disableStdOut(&s.StdoutBuf, os.Stdout)...); err != nil { + zap.S().Fatal(err) + } + + wg.Done() + }() + + if err = s.copyAndCapture(stderrIn, s.disableStdOut(&s.StderrBuf, os.Stderr)...); err != nil { + return err + } + + wg.Wait() + + return cmd.Wait() +} + +func GetHomePath(path ...string) string { + var absPath []string + + home, err := os.UserHomeDir() + if err != nil { + zap.S().Fatal(err) + } + + return filepath.Join(append(append(absPath, home), path...)...) +} + +func GetPwdPath(path ...string) string { + var absPath []string + pwd, err := os.Getwd() + if err != nil { + zap.S().Fatal(err) + } + + return filepath.Join(append(append(absPath, pwd), path...)...) +} + +func IsExists(path string, file bool) bool { + info, err := os.Stat(path) + if os.IsNotExist(err) { + return false + } + + if file { + return !info.IsDir() + } else { + return info.IsDir() + } +} + +func FindDir(path, name string) string { + fileInfo, err := os.ReadDir(path) + if err != nil { + zap.S().Fatal(err) + } + + for _, dir := range fileInfo { + if dir.IsDir() && strings.Contains(dir.Name(), name) { + return dir.Name() + } + } + + return "" +} + +func ListDir(path string, abs bool) (dirs []string, files []string, err error) { + var pathName string + + infoFiles, err := os.ReadDir(path) + if err != nil { + return nil, nil, err + } + + for _, info := range infoFiles { + if abs { + pathName = filepath.Join(path, info.Name()) + } else { + pathName = info.Name() + } + + if !info.IsDir() { + files = append(files, pathName) + } + + if info.IsDir() { + dirs = append(dirs, pathName) + } + } + + return +} + +func WalkMatch(rootPath, pattern string) ([]string, error) { + var match []string + err := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + if matched, err := filepath.Match(pattern, filepath.Base(path)); err != nil { + return err + } else if matched { + match = append(match, path) + } + + return nil + }) + if err != nil { + return nil, err + } + + return match, nil +} + +func WalkInDir(rootPath, dir, name string) ([]string, error) { + var match []string + err := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() && info.Name() == dir && IsExists(filepath.Join(path, name), true) { + match = append(match, filepath.Join(path, name)) + } + + return nil + }) + if err != nil { + return nil, err + } + + return match, nil +} + +func CopyDir(src, dst string) error { + rootDir := filepath.Base(src) + return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { + relPath := strings.Replace(path, src, "", 1) + if info.IsDir() { + return os.MkdirAll(filepath.Join(dst, rootDir, relPath), 0755) + } else { + data, err := os.ReadFile(filepath.Join(src, relPath)) + if err != nil { + return err + } + + return os.WriteFile(filepath.Join(dst, rootDir, relPath), data, 0755) + } + }) +} + +func CopyFile(src, dst string) error { + data, err := os.ReadFile(src) + if err != nil { + return err + } + + return os.WriteFile(dst, data, 0755) +} + +func MergeAgeKeys(dir string) error { + var keys []byte + + match, err := WalkMatch(dir, "*"+SopsAgeKeyExt) + if err != nil { + return err + } + + for _, path := range match { + if filepath.Base(path) != SopsAgeKeyFile { + data, err := os.ReadFile(path) + if err != nil { + return err + } + + keys = append(keys, data...) + } + } + + return os.WriteFile(filepath.Join(dir, SopsAgeKeyFile), keys, 0644) +} + +func ReadStdin(text string) string { + reader := bufio.NewReader(os.Stdin) + fmt.Printf("Enter %s: ", text) + value, _, err := reader.ReadLine() + if err != nil { + zap.S().Fatal(err) + } + + return string(value) +} + +// UnTar takes a destination path and a reader; a tar reader loops over the tar file +// creating the file structure at 'dst' along the way, and writing any files +func UnTar(dst, excludeRegexp string, r io.Reader) error { + var reg *regexp.Regexp + + gzr, err := gzip.NewReader(r) + if err != nil { + return err + } + + defer gzr.Close() + + if len(excludeRegexp) > 0 { + reg = regexp.MustCompile(excludeRegexp) + } + + tr := tar.NewReader(gzr) + for { + header, err := tr.Next() + matchRegexp := false + if len(excludeRegexp) > 0 { + matchRegexp = reg.MatchString(header.Name) + } + + switch { + case err == io.EOF: + return nil + // return any other error + case err != nil: + return err + // if the header is nil, just skip it (not sure how this happens) + case header == nil: + continue + // if the header is matching with exclude regexp, skip file or dir for unpackage + case matchRegexp: + continue + } + // the target location where the dir/file should be created + target := filepath.Join(dst, header.Name) + + // check the file type + switch header.Typeflag { + // if it's a dir, and it doesn't exist create it + case tar.TypeDir: + if _, err := os.Stat(target); err != nil { + if err := os.MkdirAll(target, 0755); err != nil { + return err + } + } + // if it's a file create it + case tar.TypeReg: + if IsExists(target, true) { + if err := os.Truncate(target, 0); err != nil { + return err + } + } + + f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) + if err != nil { + return err + } + // copy over contents + if _, err := io.Copy(f, tr); err != nil { + return err + } + // manually close here after each file operation; defering would cause each file close + // to wait until all operations have completed. + _ = f.Close() + } + } +} + +func cliArgContains(flagName string) bool { + for _, name := range strings.Split(flagName, ",") { + name = strings.TrimSpace(name) + count := utf8.RuneCountInString(name) + if count > 2 { + count = 2 + } + + flag := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) + + for _, a := range os.Args { + if a == flag { + return true + } + } + } + + return false +} + +func printFlagSuggestions(lastArg string, flags []cli.Flag, writer io.Writer) { + cur := strings.TrimPrefix(lastArg, "-") + cur = strings.TrimPrefix(cur, "-") + for _, flag := range flags { + // skip hidden flags for bool type + if boolFlag, ok := flag.(*cli.BoolFlag); ok && boolFlag.Hidden { + continue + } + // skip hidden flags for altsrc bool type + if altsrcBoolFlag, ok := flag.(*altsrc.BoolFlag); ok && altsrcBoolFlag.Hidden { + continue + } + // skip hidden flags for string type + if stringFlag, ok := flag.(*cli.StringFlag); ok && stringFlag.Hidden { + continue + } + // skip hidden flags for altsrc string type + if altsrcStringFlag, ok := flag.(*altsrc.StringFlag); ok && altsrcStringFlag.Hidden { + continue + } + + for _, name := range flag.Names() { + name = strings.TrimSpace(name) + // this will get total count utf8 letters in flag name + count := utf8.RuneCountInString(name) + if count > 2 { + count = 2 // reuse this count to generate single - or -- in flag completion + } + // if flag name has more than one utf8 letter and last argument in cli has -- prefix then + // skip flag completion for short flags example -v or -x + if strings.HasPrefix(lastArg, "--") && count == 1 { + continue + } + // match if last argument matches this flag and it is not repeated + if strings.HasPrefix(name, cur) && cur != name && !cliArgContains(name) { + flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) + _, _ = fmt.Fprintln(writer, flagCompletion) + } + } + } +} + +func ShellCompleteCustomOutput(c *cli.Context) { + if len(os.Args) > 2 { + if os.Args[len(os.Args)-2] != "" && strings.HasPrefix(os.Args[len(os.Args)-2], "-") { + printFlagSuggestions(os.Args[len(os.Args)-2], c.Command.Flags, c.App.Writer) + + return + } + } +} + +func ValidateArtifactModeDefault(c *cli.Context, errorMsg string) error { + if c.String("artifact-mode") == ArtifactModeDefault && !c.IsSet("github-token") { + if errorMsg == "" { + return fmt.Errorf(ConfigNotInitializedErrorText) + } else { + return fmt.Errorf(errorMsg) + } + } + + return nil +} + +func ValidateNArg(c *cli.Context, expectedNArg int) error { + if c.NArg() != expectedNArg { + return fmt.Errorf("exactly %d argument(s) required for '%s' command", expectedNArg, c.Command.Name) + } + + return nil +}