Update v0 (#53)

* fix: add release secrets from vault (#2)
* fix: move release please to release branches (#4)
* fix: move release please to release branches
* fix: use one file and get vault secrets
* fix: abstract OS file functions (#6)
* fix: add automation to generate sub issues (#7)
* fix: use new path to attach sub issue (#9)
* fix: use the API endpoint to attach the sub issue (#11)
* fix: add console line to see context (#13)
* fix: use the full payload issue (#15)
* fix: use a different context (#16)
* fix: use the proper variable name (#18)
* fix: create issue when a pull request hits main (#20)
* fix: give issue write permissions (#21)
* fix: give issue write permissions
* fix: try pull request target
* fix: update pull request template (#22)
* fix: assign users to main pr (#25)
* fix: add back port pr (#27)
* fix: bump dependency from 8 to 27 in tools (#29)
* fix: remove try (#31)
* fix: use rest request to get sub issues (#33)
* fix: use new search API and handle empty label (#34)
* fix: remove backport info from pr template (#35)
* fix: create a new array to save labels (#36)
* fix: use API directly to query issues (#37)
* fix: add console log to check context (#38)
* fix: remove console log and change count property (#39)
* fix: add team members individually to issue (#40)
* fix: remove console line (#41)
* fix: remove console line
* fix: assign to me
* fix: add console line converting object to string (#42)
* fix: remove unnecessary data (#45)
* fix: correct sub issue address (#47)
* fix: resolve merge conflicts in backport (#50)
* fix: rephrase cherry pick workflow

Signed-off-by: matttrach <matt.trachier@suse.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Matt Trachier 2025-08-20 16:25:37 -05:00 committed by GitHub
parent 0533008f61
commit 3adcf2fb12
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 503 additions and 247 deletions

View File

@ -1,17 +1,9 @@
#!/bin/env sh
alias gs='git status'
alias gd='git diff'
alias gc='git checkout'
alias tf='terraform'
alias tfa='if [ -f ssh_key ]; then chmod 600 ssh_key && ssh-add ssh_key; fi; terraform init; terraform apply --auto-approve'
alias tfa='terraform apply --auto-approve'
alias tfd='terraform destroy --auto-approve'
alias tfp='terraform init || terraform providers && terraform validate && terraform plan'
alias tfr='terraform destroy --auto-approve;if [ -f ssh_key ]; then chmod 600 ssh_key && ssh-add ssh_key; fi; terraform init; terraform apply --auto-approve'
alias tfl='terraform state list'
alias k='kubectl'
alias tt='run_tests'
# expects AGE_ variables to be set, see .variables and .rcs
alias es='encrypt_secrets' # looks in the secret file list and converts the files into encrypted ones, see .functions
alias ds='decrypt_secrets' # looks in the secret file list and converts all the encrtypted files in to unencrypted ones, see .functions
alias ef='encrypt_file' # see .functions
alias cl='clear_local' # clears all of the temporary files from the directory, see .functions
alias sc='shell_check' # runs shellcheck -x on all files with a shbang

2
.github/CODEOWNERS vendored
View File

@ -1 +1 @@
* @rancher/k3s
* @rancher/terraform-maintainers

View File

@ -1,16 +1,22 @@
## Related Issue
Fixes # <!-- INSERT ISSUE NUMBER -->
Addresses #1234 (main issue)
## Releases
If this PR should be released, please add labels for each release branch it targets.
Use the 'release/v0' tags, not the 'version/v0' tags.
## Description
In plain English, describe your approach to addressing the issue linked above. For example, if you made a particular design decision, let us know why you chose this path instead of another solution.
Describe your change and how it addresses the issue linked above.
<!-- heimdall_github_prtemplate:grc-pci_dss-2024-01-05 -->
## Rollback Plan
## Testing
- [ ] If a change needs to be reverted, we will roll out an update to the code within 7 days.
Please describe how you verified this change or why testing isn't relevant.
## Changes to Security Controls
## Breaking
Does this change alter an interface that users of the provider will need to adjust to?
Will there be any existing configurations broken by this change?
Are there any changes to security controls (access controls, encryption, logging) in this pull request? If so, explain.

106
.github/workflows/backport-prs.yml vendored Normal file
View File

@ -0,0 +1,106 @@
name: 'Auto Cherry-Pick to Release Branches'
on:
push:
branches: ['main']
jobs:
create-cherry-pick-prs:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: 'Wait for merge to settle'
run: sleep 10
- name: 'Checkout Repository'
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 'Find Issues and Create Cherry-Pick PRs'
uses: actions/github-script@v7
with:
script: |
const execSync = require('child_process').execSync;
const owner = context.repo.owner;
const repo = context.repo.repo;
const mergeCommitSha = context.payload.head_commit.id;
const { data: associatedPrs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner,
repo,
commit_sha: mergeCommitSha
});
const pr = associatedPrs.find(p => p.base.ref === 'main' && p.merged_at);
if (!pr) {
core.info(`No merged PR found for commit ${mergeCommitSha}. This may have been a direct push. Exiting.`);
return;
}
core.info(`Found associated PR: #${pr.number}`);
// https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-issues-and-pull-requests
core.info(`Searching for 'internal/main' issue linked to PR #${pr.number}`);
const { data: searchResults } = await github.request('GET /search/issues', {
q: `is:issue label:"internal/main" repo:${owner}/${repo} in:body #${pr.number}`,
advanced_search: true,
headers: {
'X-GitHub-Api-Version': '2022-11-28'
}
});
if (searchResults.total_count === 0) {
core.info(`No 'internal/main' issue found for PR #${pr.number}. Exiting.`);
return;
}
const mainIssue = searchResults.items[0];
core.info(`Found main issue: #${mainIssue.number}`);
// https://docs.github.com/en/rest/issues/sub-issues?apiVersion=2022-11-28#add-sub-issue
core.info(`Fetching sub-issues for main issue #${mainIssue.number}`);
const { data: subIssues } = await github.request('GET /repos/{owner}/{repo}/issues/{issue_number}/sub_issues', {
owner: owner,
repo: repo,
issue_number: mainIssue.number,
headers: {
'X-GitHub-Api-Version': '2022-11-28'
}
});
if (subIssues.length === 0) {
core.info(`No sub-issues found for issue #${mainIssue.number}. Exiting.`);
return;
}
core.info(`Found ${subIssues.length} sub-issues.`);
for (const subIssue of subIssues) {
const subIssueNumber = subIssue.number;
// Find the release label directly on the sub-issue object
const releaseLabel = subIssue.labels.find(label => label.name.startsWith('release/v'));
if (!releaseLabel) {
core.warning(`Sub-issue #${subIssueNumber} has no 'release/v...' label. Skipping.`);
continue;
}
const targetBranch = releaseLabel.name
core.info(`Processing sub-issue #${subIssueNumber} for target branch: ${targetBranch}`);
const newBranchName = `backport-${pr.number}-${targetBranch.replace(/\//g, '-')}`;
execSync(`git config user.name "github-actions[bot]"`);
execSync(`git config user.email "github-actions[bot]@users.noreply.github.com"`);
execSync(`git fetch origin ${targetBranch}`);
execSync(`git checkout -b ${newBranchName} origin/${targetBranch}`);
execSync(`git cherry-pick -x ${mergeCommitSha} -X theirs`);
execSync(`git push origin ${newBranchName}`);
core.info(`Creating pull request for branch ${newBranchName} targeting ${targetBranch}...`);
const { data: newPr } = await github.rest.pulls.create({
owner,
repo,
title: pr.title,
head: newBranchName,
base: targetBranch,
body: "This pull request cherry-picks the changes from #" + pr.number + " into " + targetBranch + "\n" +
"WARNING!: to avoid having to resolve merge conflicts this PR is generated with `git cherry-pick -X theirs`.\n" +
"Please make sure to carefully inspect this PR so that you don't revert anything!",
assignees: ['terraform-maintainers']
});
}

44
.github/workflows/backport.yml vendored Normal file
View File

@ -0,0 +1,44 @@
name: Backports
# This workflow generates "backport" issues when a release branch label is added to an issue
on:
issues:
types: [labeled] # triggered when any label is added to an issue
jobs:
create-issue:
runs-on: ubuntu-latest
if: ${{ github.event.label.name == 'version/v0' }}
steps:
- name: Create GitHub Issue
uses: actions/github-script@v7
with:
script: |
const parentIssue = context.payload.issue;
const parentIssueTitle = parentIssue.title;
const parentIssueNumber = parentIssue.number;
const repo = context.repo.repo;
const owner = context.repo.owner;
// Note: can't get terraform-maintainers team, the default token can't access org level objects
// Create the sub-issue
const newIssue = await github.rest.issues.create({
owner: owner,
repo: repo,
title: "Backport #" + parentIssueNumber + " to release/v0",
body: "Backport #" + parentIssueNumber + " to release/v0",
labels: ['release/v0'],
assignees: ['matttrach']
});
const subIssueId = newIssue.data.id;
// Attach the sub-issue to the parent using API request
await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/sub_issues', {
owner: owner,
repo: repo,
issue_number: parentIssueNumber,
sub_issue_id: subIssueId,
headers: {
'X-GitHub-Api-Version': '2022-11-28'
}
});

View File

@ -1,21 +0,0 @@
# DO NOT EDIT - This GitHub Workflow is managed by automation
# https://github.com/hashicorp/terraform-devex-repos
name: Issue Comment Triage
on:
issue_comment:
types: [created]
jobs:
issue_comment_triage:
runs-on: ubuntu-latest
env:
# issue_comment events are triggered by comments on issues and pull requests. Checking the
# value of github.event.issue.pull_request tells us whether the issue is an issue or is
# actually a pull request, allowing us to dynamically set the gh subcommand:
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment-on-issues-only-or-pull-requests-only
COMMAND: ${{ github.event.issue.pull_request && 'pr' || 'issue' }}
GH_TOKEN: ${{ github.token }}
steps:
- name: 'Remove waiting-response on comment'
run: gh ${{ env.COMMAND }} edit ${{ github.event.issue.html_url }} --remove-label waiting-response

41
.github/workflows/main-issue.yml vendored Normal file
View File

@ -0,0 +1,41 @@
name: MainIssue
# This workflow generates a "main" issue when a PR is created targeting main.
on:
pull_request_target:
branches: [main]
types: [opened]
jobs:
generate-issue:
name: 'Create Main Issue'
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/github-script@v7
with:
script: |
const repo = context.repo.repo;
const owner = context.repo.owner;
const pr = context.payload.pull_request;
const newLabels = ['internal/main'];
const releaseLabel = pr.labels.find(label => label.name.startsWith('release/v'));
if (releaseLabel) {
const versionLabel = releaseLabel.name.replace('release/', 'version/');
newLabels.push(versionLabel);
}
// Note: can't get terraform-maintainers team, the default token can't access org level objects
// Create the main issue
// https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#create-an-issue
// Note: issues can't have teams assigned to them
const newIssue = await github.rest.issues.create({
owner: owner,
repo: repo,
title: pr.title,
body: "This is the main issue tracking #" + pr.number + " \n\n" +
"Please add labels indicating the release versions eg. 'version/v0' \n\n" +
"Please add comments for user issues which this issue addresses. \n\n" +
"Description copied from PR: \n" + pr.body,
labels: newLabels,
assignees: ['matttrach']
});

View File

@ -3,7 +3,8 @@ name: release
on:
push:
branches:
- main
- release/v0
- release/v1
permissions: write-all
@ -76,6 +77,43 @@ jobs:
repo: "${{ github.event.repository.name }}",
body: "Tests Failed!"
})
- name: retrieve GPG Credentials
uses: rancher-eio/read-vault-secrets@main
with:
secrets: |
secret/data/github/repo/rancher/${{ github.repository }}/signing/gpg passphrase | GPG_PASSPHRASE ;
secret/data/github/repo/rancher/${{ github.repository }}/signing/gpg privateKeyId | GPG_KEY_ID;
secret/data/github/repo/rancher/${{ github.repository }}/signing/gpg privateKey | GPG_KEY;
- name: import_gpg_key
if: steps.release-please.outputs.pr && (steps.run-unit-tests.conclusion == 'success') && (steps.run-acc-tests.conclusion == 'success')
env:
GPG_PASSPHRASE: ${{ env.GPG_PASSPHRASE }}
GPG_KEY_ID: ${{ env.GPG_KEY_ID }}
GPG_KEY: ${{ env.GPG_KEY }}
run: |
cleanup() {
# clear history just in case
history -c
}
trap cleanup EXIT TERM
# sanitize variables
if [ -z "${GPG_PASSPHRASE}" ]; then echo "gpg passphrase empty"; exit 1; fi
if [ -z "${GPG_KEY_ID}" ]; then echo "key id empty"; exit 1; fi
if [ -z "${GPG_KEY}" ]; then echo "key contents empty"; exit 1; fi
echo "Importing gpg key"
echo "${GPG_KEY}" | gpg --import --batch > /dev/null || { echo "Failed to import GPG key"; exit 1; }
- name: Run GoReleaser
if: steps.release-please.outputs.pr && (steps.run-unit-tests.conclusion == 'success') && (steps.run-acc-tests.conclusion == 'success')
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 https://github.com/goreleaser/goreleaser-action
with:
args: release --snapshot --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GPG_KEY_ID: ${{ env.GPG_KEY_ID }}
GPG_PASSPHRASE: ${{ env.GPG_PASSPHRASE }}
# These run after release-please generates a release, so when the release PR is merged
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@ -87,13 +125,19 @@ jobs:
with:
go-version-file: 'go.mod'
cache: true
- name: retrieve GPG Credentials
uses: rancher-eio/read-vault-secrets@main
with:
secrets: |
secret/data/github/repo/rancher/${{ github.repository }}/signing/gpg passphrase | GPG_PASSPHRASE ;
secret/data/github/repo/rancher/${{ github.repository }}/signing/gpg privateKeyId | GPG_KEY_ID;
secret/data/github/repo/rancher/${{ github.repository }}/signing/gpg privateKey | GPG_KEY;
- name: import_gpg_key
if: steps.release-please.outputs.version
id: import_gpg_key
env:
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }}
GPG_KEY: ${{ secrets.GPG_KEY }}
GPG_PASSPHRASE: ${{ env.GPG_PASSPHRASE }}
GPG_KEY_ID: ${{ env.GPG_KEY_ID }}
GPG_KEY: ${{ env.GPG_KEY }}
run: |
cleanup() {
# clear history just in case
@ -115,5 +159,5 @@ jobs:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
GPG_KEY_ID: ${{ env.GPG_KEY_ID }}
GPG_PASSPHRASE: ${{ env.GPG_PASSPHRASE }}

View File

@ -1,11 +1,10 @@
# Copyright (c) HashiCorp, Inc.
# Visit https://goreleaser.com for documentation on how to customize this
# behavior.
# https://goreleaser.com for documentation
version: 2
before:
hooks:
# this is just an example and not a requirement for provider building/publishing
- go mod tidy
builds:
- env:
@ -25,12 +24,8 @@ builds:
- darwin
goarch:
- amd64
- '386'
- arm
- arm64
ignore:
- goos: darwin
goarch: '386'
binary: '{{ .ProjectName }}_v{{ .Version }}'
archives:
- formats: [ 'zip' ]
@ -55,6 +50,10 @@ signs:
- "${signature}"
- "--sign"
- "${artifact}"
snapshot:
# "snapshot" is the type of release we use for release candidates
# that are generated when a release branch gets a new merge
name_template: "{{ .ProjectName }}_{{ .ShortCommit }}"
release:
extra_files:
- glob: 'terraform-registry-manifest.json'

View File

@ -10,3 +10,8 @@ goreleaser
terraform
tflint
gorelease
repo
pr
assignees
backport
url

View File

@ -34,7 +34,7 @@ resource "file_local" "example" {
- `directory` (String) The directory where the file will be placed, defaults to the current working directory.
- `hmac_secret_key` (String, Sensitive) A string used to generate the file identifier, you can pass this value in the environment variable `TF_FILE_HMAC_SECRET_KEY`.The provider will use a hard coded value as the secret key for unprotected files.
- `id` (String) Identifier derived from sha256+HMAC hash of file contents. When setting 'protected' to true this argument is required. However, when 'protected' is false then this should be left empty (computed by the provider).
- `mode` (String) The file permissions to assign to the file, defaults to '0600'.
- `permissions` (String) The file permissions to assign to the file, defaults to '0600'.
- `protected` (Boolean) Whether or not to fail update or create if the calculated id doesn't match the given id.When this is true, the 'id' field is required and must match what we calculate as the hash at both create and update times.If the 'id' configured doesn't match what we calculate then the provider will error rather than updating or creating the file.When setting this to true, you will need to either set the `TF_FILE_HMAC_SECRET_KEY` environment variable or set the hmac_secret_key argument.
## Import

View File

@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1755082269,
"narHash": "sha256-Ix7ALeaxv9tW4uBKWeJnaKpYZtZiX4H4Q/MhEmj4XYA=",
"lastModified": 1755577059,
"narHash": "sha256-5hYhxIpco8xR+IpP3uU56+4+Bw7mf7EMyxS/HqUYHQY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d74de548348c46cf25cb1fcc4b74f38103a4590d",
"rev": "97eb7ee0da337d385ab015a23e15022c865be75c",
"type": "github"
},
"original": {

View File

@ -34,20 +34,73 @@ import (
var _ resource.Resource = &LocalResource{}
var _ resource.ResourceWithImportState = &LocalResource{}
// type FileClient struct{}
// An interface for defining custom file managers.
type fileClient interface {
Create(directory string, name string, data string, permissions string) error
// If file isn't found the error message must have err.Error() == "file not found"
Read(directory string, name string) (string, string, error) // permissions, contents, error
Update(currentDirectory string, currentName string, newDirectory string, newName string, data string, permissions string) error
Delete(directory string, name string) error
}
// func (f *FileClient) Create() {}
// func (f *FileClient) Read() {}
// func (f *FileClient) Update() {}
// func (f *FileClient) Delete() {}
// The default fileClient, using the os package.
type osFileClient struct{}
var _ fileClient = &osFileClient{} // make sure the osFileClient implements the fileClient
func (c *osFileClient) Create(directory string, name string, data string, permissions string) error {
path := filepath.Join(directory, name)
modeInt, err := strconv.ParseUint(permissions, 8, 32)
if err != nil {
return err
}
return os.WriteFile(path, []byte(data), os.FileMode(modeInt))
}
func (c *osFileClient) Read(directory string, name string) (string, string, error) {
path := filepath.Join(directory, name)
info, err := os.Stat(path)
if err != nil && os.IsNotExist(err) {
return "", "", fmt.Errorf("file not found")
}
if err != nil {
return "", "", err
}
mode := fmt.Sprintf("%#o", info.Mode().Perm())
contents, err := os.ReadFile(path)
if err != nil {
return "", "", err
}
return mode, string(contents), nil
}
func (c *osFileClient) Update(currentDirectory string, currentName string, newDirectory string, newName string, data string, permissions string) error {
currentPath := filepath.Join(currentDirectory, currentName)
newPath := filepath.Join(newDirectory, newName)
if currentPath != newPath {
err := os.Rename(currentPath, newPath)
if err != nil {
return err
}
}
modeInt, err := strconv.ParseUint(permissions, 8, 32)
if err != nil {
return err
}
if err = os.WriteFile(newPath, []byte(data), os.FileMode(modeInt)); err != nil {
return err
}
return nil
}
func (c *osFileClient) Delete(directory string, name string) error {
path := filepath.Join(directory, name)
return os.Remove(path)
}
func NewLocalResource() resource.Resource {
return &LocalResource{}
}
// LocalResource defines the resource implementation.
// This facilitates the LocalResource class, it can now be used in functions with *LocalResource.
type LocalResource struct{}
type LocalResource struct {
client fileClient
}
// LocalResourceModel describes the resource data model.
type LocalResourceModel struct {
@ -55,10 +108,9 @@ type LocalResourceModel struct {
Name types.String `tfsdk:"name"`
Contents types.String `tfsdk:"contents"`
Directory types.String `tfsdk:"directory"`
Mode types.String `tfsdk:"mode"`
Permissions types.String `tfsdk:"permissions"`
HmacSecretKey types.String `tfsdk:"hmac_secret_key"`
Protected types.Bool `tfsdk:"protected"`
// Fake types.Bool `tfsdk:"fake"`
}
func (r *LocalResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
@ -84,7 +136,7 @@ func (r *LocalResource) Schema(ctx context.Context, req resource.SchemaRequest,
Computed: true, // whenever an argument has a default value it should have Computed: true
Default: stringdefault.StaticString("."),
},
"mode": schema.StringAttribute{
"permissions": schema.StringAttribute{
MarkdownDescription: "The file permissions to assign to the file, defaults to '0600'.",
Optional: true,
Computed: true,
@ -141,6 +193,10 @@ func (r *LocalResource) Configure(ctx context.Context, req resource.ConfigureReq
if req.ProviderData == nil {
return
}
// Allow the ability to inject a file client, but use the osFileClient by default.
if r.client == nil {
r.client = &osFileClient{}
}
}
// We should:
@ -161,7 +217,7 @@ func (r *LocalResource) Create(ctx context.Context, req resource.CreateRequest,
name := plan.Name.ValueString()
directory := plan.Directory.ValueString()
contents := plan.Contents.ValueString()
modeString := plan.Mode.ValueString()
permString := plan.Permissions.ValueString()
hmacSecretKey := plan.HmacSecretKey.ValueString()
protected := plan.Protected.ValueBool()
@ -190,14 +246,8 @@ func (r *LocalResource) Create(ctx context.Context, req resource.CreateRequest,
plan.HmacSecretKey = types.StringValue("")
}
localFilePath := filepath.Join(directory, name)
modeInt, err := strconv.ParseUint(modeString, 8, 32)
if err != nil {
resp.Diagnostics.AddError("Error reading file mode from config: ", err.Error())
return
}
if err = os.WriteFile(localFilePath, []byte(contents), os.FileMode(modeInt)); err != nil {
resp.Diagnostics.AddError("Error writing file: ", err.Error())
if err = r.client.Create(directory, name, contents, permString); err != nil {
resp.Diagnostics.AddError("Error creating file: ", err.Error())
return
}
@ -220,28 +270,24 @@ func (r *LocalResource) Read(ctx context.Context, req resource.ReadRequest, resp
sName := state.Name.ValueString()
sDirectory := state.Directory.ValueString()
sContents := state.Contents.ValueString()
sMode := state.Mode.ValueString()
sPerm := state.Permissions.ValueString()
sHmacSecretKey := state.HmacSecretKey.ValueString()
sFilePath := filepath.Join(sDirectory, sName)
// If Possible, we should avoid reading the file into memory
// The "real" (non-calculated) parts of the file are the path, the contents, and the mode
// If the file doesn't exist at the path, then we need to (re)create it
if _, err := os.Stat(sFilePath); os.IsNotExist(err) {
perm, contents, err := r.client.Read(sDirectory, sName)
if err != nil && err.Error() == "File not found." {
resp.State.RemoveResource(ctx)
return
}
// If the file's contents have changed, then we need to update the state
c, err := os.ReadFile(sFilePath)
if err != nil {
resp.Diagnostics.AddError("Error reading file: ", err.Error())
return
}
contents := string(c)
if contents != sContents {
// update state with actual contents
state.Contents = types.StringValue(contents)
@ -260,16 +306,9 @@ func (r *LocalResource) Read(ctx context.Context, req resource.ReadRequest, resp
state.Id = types.StringValue(id)
}
// If the file's mode has changed, then we need to update the state
inf, err := os.Stat(sFilePath)
if err != nil {
resp.Diagnostics.AddError("Error reading file stat: ", err.Error())
return
}
mode := fmt.Sprintf("%#o", inf.Mode().Perm())
if mode != sMode {
if perm != sPerm {
// update the state with the actual mode
state.Mode = types.StringValue(mode)
state.Permissions = types.StringValue(perm)
}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
@ -292,12 +331,10 @@ func (r *LocalResource) Update(ctx context.Context, req resource.UpdateRequest,
cName := config.Name.ValueString()
cContents := config.Contents.ValueString()
cDirectory := config.Directory.ValueString()
cMode := config.Mode.ValueString()
cPerm := config.Permissions.ValueString()
cHmacSecretKey := config.HmacSecretKey.ValueString()
cProtected := config.Protected.ValueBool()
cFilePath := filepath.Join(cDirectory, cName)
cKey := cHmacSecretKey
if cKey == "" {
cKey = os.Getenv("TF_FILE_HMAC_SECRET_KEY")
@ -318,6 +355,7 @@ func (r *LocalResource) Update(ctx context.Context, req resource.UpdateRequest,
config.HmacSecretKey = types.StringValue("")
}
// Read updates state with reality, so state = reality
var reality LocalResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &reality)...)
if resp.Diagnostics.HasError() {
@ -325,47 +363,13 @@ func (r *LocalResource) Update(ctx context.Context, req resource.UpdateRequest,
}
rName := reality.Name.ValueString()
rContents := reality.Contents.ValueString()
rDirectory := reality.Directory.ValueString()
rMode := reality.Mode.ValueString()
rFilePath := filepath.Join(rDirectory, rName)
if rFilePath != cFilePath {
// config is changing the file path, we need to move the file
err := os.Rename(rFilePath, cFilePath)
if err != nil {
resp.Diagnostics.AddError("Error moving file: ", err.Error())
return
}
} // the config's file path (cFilePath) is now accurate
if rMode != cMode {
// the config is changing the mode
modeInt, err := strconv.ParseUint(cMode, 8, 32)
if err != nil {
resp.Diagnostics.AddError("Error reading file mode from config: ", err.Error())
return
}
err = os.Chmod(cFilePath, os.FileMode(modeInt))
if err != nil {
resp.Diagnostics.AddError("Error changing file mode: ", err.Error())
return
}
} // the config's mode (cMode) is now accurate
if cContents != rContents {
// config is changing the contents
modeInt, err := strconv.ParseUint(cMode, 8, 32)
if err != nil {
resp.Diagnostics.AddError("Error reading file mode from config: ", err.Error())
return
}
if err = os.WriteFile(cFilePath, []byte(cContents), os.FileMode(modeInt)); err != nil {
resp.Diagnostics.AddError("Error writing file: ", err.Error())
return
}
} // the config's contents (cContents) are now accurate
err := r.client.Update(rDirectory, rName, cDirectory, cName, cContents, cPerm)
if err != nil {
resp.Diagnostics.AddError("Error updating file: ", err.Error())
return
}
// the path, mode, and contents are all of the "real" parts of the file
// the id is calculated from the secret key and contents,
@ -396,8 +400,6 @@ func (r *LocalResource) Delete(ctx context.Context, req resource.DeleteRequest,
}
contents := state.Contents.ValueString()
localFilePath := filepath.Join(directory, name)
// we need to validate the id before we can delete a protected file
if protected {
err := validateProtected(protected, id, key, contents)
@ -407,7 +409,7 @@ func (r *LocalResource) Delete(ctx context.Context, req resource.DeleteRequest,
}
}
if err := os.Remove(localFilePath); err != nil {
if err := r.client.Delete(directory, name); err != nil {
tflog.Error(ctx, "Failed to delete file: "+err.Error())
return
}

View File

@ -4,8 +4,7 @@ package provider
import (
"context"
"os"
"path/filepath"
"fmt"
"slices"
"strconv"
"testing"
@ -19,7 +18,7 @@ import (
const (
defaultId = ""
defaultDirectory = "."
defaultMode = "0600"
defaultPerm = "0600"
defaultProtected = "false"
defaultHmacSecretKey = ""
)
@ -73,21 +72,20 @@ func TestLocalSchema(t *testing.T) {
func TestLocalResourceCreate(t *testing.T) {
t.Run("Create function", func(t *testing.T) {
testCases := []struct {
name string
fit LocalResource
have resource.CreateRequest
want resource.CreateResponse
tearDownPath string
name string
fit LocalResource
have resource.CreateRequest
want resource.CreateResponse
}{
{
"Basic",
LocalResource{},
LocalResource{client: &memoryFileClient{}},
// have
getCreateRequest(t, map[string]string{
"id": defaultId,
"name": "test_basic.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"permissions": defaultPerm,
"contents": "this is a basic test",
"protected": defaultProtected,
"hmac_secret_key": defaultHmacSecretKey, // this should use the hard coded hmac secret key for unprotected files
@ -97,22 +95,21 @@ func TestLocalResourceCreate(t *testing.T) {
"id": "3de642fb91d2fb0ce02fe66c3d19ebdf44cbc6a2ebcc2dad22f1950b67c1217f",
"name": "test_basic.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"permissions": defaultPerm,
"contents": "this is a basic test",
"protected": defaultProtected,
"hmac_secret_key": defaultHmacSecretKey,
}),
filepath.Join(defaultDirectory, "test_basic.tmp"),
},
{
"Protected",
LocalResource{},
LocalResource{client: &osFileClient{}},
// have
getCreateRequest(t, map[string]string{
"id": "4ccd8ec7ea24e0524c8aba459fbf3a2649ec3cd96a1c8f9dfb326cc57a9d3127",
"name": "test_protected.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"permissions": defaultPerm,
"contents": "this is a test",
"protected": "true",
"hmac_secret_key": "this-is-a-test-key",
@ -122,22 +119,21 @@ func TestLocalResourceCreate(t *testing.T) {
"id": "4ccd8ec7ea24e0524c8aba459fbf3a2649ec3cd96a1c8f9dfb326cc57a9d3127",
"name": "test_protected.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"permissions": defaultPerm,
"contents": "this is a test",
"protected": "true",
"hmac_secret_key": "this-is-a-test-key",
}),
filepath.Join(defaultDirectory, "test_protected.tmp"),
},
{
"Protected using key from environment",
LocalResource{},
LocalResource{client: &memoryFileClient{}},
// have
getCreateRequest(t, map[string]string{
"id": "59fed8691a76c7693fc9dcd4fda28390a1fd3090114bc64f3e5a3abe312a92f5",
"name": "test_protected.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"permissions": defaultPerm,
"contents": "this is a test",
"protected": "true",
"hmac_secret_key": defaultHmacSecretKey, // this relies on TF_FILE_HMAC_SECRET_KEY=thisisasupersecretkey in your environment
@ -147,12 +143,11 @@ func TestLocalResourceCreate(t *testing.T) {
"id": "59fed8691a76c7693fc9dcd4fda28390a1fd3090114bc64f3e5a3abe312a92f5",
"name": "test_protected.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"permissions": defaultPerm,
"contents": "this is a test",
"protected": "true",
"hmac_secret_key": defaultHmacSecretKey,
}),
filepath.Join(defaultDirectory, "test_protected.tmp"),
},
}
for _, tc := range testCases {
@ -163,12 +158,18 @@ func TestLocalResourceCreate(t *testing.T) {
}
plannedProtected := plannedState.Protected.ValueBool()
plannedHmacSecretKey := plannedState.HmacSecretKey.ValueString()
plannedDirectory := plannedState.Directory.ValueString()
plannedName := plannedState.Name.ValueString()
if plannedProtected && plannedHmacSecretKey == "" {
t.Setenv("TF_FILE_HMAC_SECRET_KEY", "thisisasupersecretkey")
}
r := getCreateResponseContainer()
tc.fit.Create(context.Background(), tc.have, &r)
defer teardown(tc.tearDownPath)
defer func() {
if err := tc.fit.client.Delete(plannedDirectory, plannedName); err != nil {
t.Errorf("Error cleaning up: %v", err)
}
}()
got := r
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("Create() mismatch (-want +got):\n%s", diff)
@ -181,22 +182,21 @@ func TestLocalResourceCreate(t *testing.T) {
func TestLocalResourceRead(t *testing.T) {
t.Run("Read function", func(t *testing.T) {
testCases := []struct {
name string
fit LocalResource
have resource.ReadRequest
want resource.ReadResponse
setup map[string]string
tearDownPath string
name string
fit LocalResource
have resource.ReadRequest
want resource.ReadResponse
setup map[string]string
}{
{
"Unprotected",
LocalResource{},
LocalResource{client: &memoryFileClient{}},
// have
getReadRequest(t, map[string]string{
"id": "60cef95046105ff4522c0c1f1aeeeba43d0d729dbcabdd8846c317c98cac60a2",
"name": "read.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"permissions": defaultPerm,
"contents": "this is an unprotected read test",
"protected": defaultProtected,
"hmac_secret_key": defaultHmacSecretKey,
@ -206,27 +206,27 @@ func TestLocalResourceRead(t *testing.T) {
"id": "60cef95046105ff4522c0c1f1aeeeba43d0d729dbcabdd8846c317c98cac60a2",
"name": "read.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"permissions": defaultPerm,
"contents": "this is an unprotected read test",
"protected": defaultProtected,
"hmac_secret_key": defaultHmacSecretKey,
}),
map[string]string{
"mode": defaultMode,
"path": filepath.Join(defaultDirectory, "read.tmp"),
"contents": "this is an unprotected read test",
"mode": defaultPerm,
"directory": defaultDirectory,
"name": "read.tmp",
"contents": "this is an unprotected read test",
},
filepath.Join(defaultDirectory, "read.tmp"),
},
{
"Protected",
LocalResource{},
LocalResource{client: &memoryFileClient{}},
// have
getReadRequest(t, map[string]string{
"id": "ec4407ba53b2c40ac2ac18ff7372a6fe6e4f7f8aa04f340503aefc7d9a5fa4e1",
"name": "read_protected.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"permissions": defaultPerm,
"contents": "this is a protected read test",
"protected": "true",
"hmac_secret_key": "this-is-a-test-key",
@ -236,28 +236,28 @@ func TestLocalResourceRead(t *testing.T) {
"id": "ec4407ba53b2c40ac2ac18ff7372a6fe6e4f7f8aa04f340503aefc7d9a5fa4e1",
"name": "read_protected.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"permissions": defaultPerm,
"contents": "this is a protected read test",
"protected": "true",
"hmac_secret_key": "this-is-a-test-key",
}),
// reality
map[string]string{
"mode": defaultMode,
"path": filepath.Join(defaultDirectory, "read_protected.tmp"),
"contents": "this is a protected read test",
"mode": defaultPerm,
"directory": defaultDirectory,
"name": "read_protected.tmp",
"contents": "this is a protected read test",
},
filepath.Join(defaultDirectory, "read_protected.tmp"),
},
{
"Protected with content update",
LocalResource{},
LocalResource{client: &memoryFileClient{}},
// have
getReadRequest(t, map[string]string{
"id": "ec4407ba53b2c40ac2ac18ff7372a6fe6e4f7f8aa04f340503aefc7d9a5fa4e1",
"name": "read_protected_content.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"permissions": defaultPerm,
"contents": "this is a protected read test",
"protected": "true",
"hmac_secret_key": "this-is-a-test-key",
@ -267,28 +267,28 @@ func TestLocalResourceRead(t *testing.T) {
"id": "84326116e261654e44ca3cb73fa026580853794062d472bc817b7ec2c82ff648",
"name": "read_protected_content.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"permissions": defaultPerm,
"contents": "this is a change in contents in the real file",
"protected": "true",
"hmac_secret_key": "this-is-a-test-key",
}),
// reality
map[string]string{
"mode": defaultMode,
"path": filepath.Join(defaultDirectory, "read_protected_content.tmp"),
"contents": "this is a change in contents in the real file",
"mode": defaultPerm,
"directory": defaultDirectory,
"name": "read_protected_content.tmp",
"contents": "this is a change in contents in the real file",
},
filepath.Join(defaultDirectory, "read_protected_content.tmp"),
},
{
"Protected with mode update",
LocalResource{},
LocalResource{client: &memoryFileClient{}},
// have
getReadRequest(t, map[string]string{
"id": "ec4407ba53b2c40ac2ac18ff7372a6fe6e4f7f8aa04f340503aefc7d9a5fa4e1",
"name": "read_protected_mode.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"permissions": defaultPerm,
"contents": "this is a protected read test",
"protected": "true",
"hmac_secret_key": "this-is-a-test-key",
@ -298,24 +298,30 @@ func TestLocalResourceRead(t *testing.T) {
"id": "ec4407ba53b2c40ac2ac18ff7372a6fe6e4f7f8aa04f340503aefc7d9a5fa4e1",
"name": "read_protected_mode.tmp",
"directory": defaultDirectory,
"mode": "0755",
"permissions": "0755",
"contents": "this is a protected read test",
"protected": "true",
"hmac_secret_key": "this-is-a-test-key",
}),
// reality
map[string]string{
"mode": "0755",
"path": filepath.Join(defaultDirectory, "read_protected_mode.tmp"),
"contents": "this is a protected read test",
"mode": "0755",
"directory": defaultDirectory,
"name": "read_protected_mode.tmp",
"contents": "this is a protected read test",
},
filepath.Join(defaultDirectory, "read_protected_mode.tmp"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
setup(tc.setup)
defer teardown(tc.tearDownPath)
if err := tc.fit.client.Create(tc.setup["directory"], tc.setup["name"], tc.setup["contents"], tc.setup["mode"]); err != nil {
t.Errorf("Error setting up: %v", err)
}
defer func() {
if err := tc.fit.client.Delete(tc.setup["directory"], tc.setup["name"]); err != nil {
t.Errorf("Error tearing down: %v", err)
}
}()
r := getReadResponseContainer()
tc.fit.Read(context.Background(), tc.have, &r)
got := r
@ -326,27 +332,25 @@ func TestLocalResourceRead(t *testing.T) {
}
})
}
func TestLocalResourceUpdate(t *testing.T) {
t.Run("Update function", func(t *testing.T) {
testCases := []struct {
name string
fit LocalResource
have resource.UpdateRequest
want resource.UpdateResponse
setup map[string]string
tearDownPath string
name string
fit LocalResource
have resource.UpdateRequest
want resource.UpdateResponse
setup map[string]string
}{
{
"Basic test",
LocalResource{},
LocalResource{client: &memoryFileClient{}},
// have
getUpdateRequest(t, map[string]map[string]string{
"priorState": {
"id": defaultId,
"name": "update_basic.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"permissions": defaultPerm,
"contents": "this is an update test",
"protected": defaultProtected,
"hmac_secret_key": defaultHmacSecretKey,
@ -355,7 +359,7 @@ func TestLocalResourceUpdate(t *testing.T) {
"id": defaultId,
"name": "update_basic.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"permissions": defaultPerm,
"contents": "this is a basic update test",
"protected": defaultProtected,
"hmac_secret_key": defaultHmacSecretKey,
@ -366,24 +370,30 @@ func TestLocalResourceUpdate(t *testing.T) {
"id": "0ec41eee6c157a3f7e50b78d586ee2ddb4d6e93b6de8bdf6d9354cf720e89549",
"name": "update_basic.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"permissions": defaultPerm,
"contents": "this is a basic update test",
"protected": defaultProtected,
"hmac_secret_key": defaultHmacSecretKey,
}),
// setup
map[string]string{
"mode": defaultMode,
"path": filepath.Join(defaultDirectory, "update_basic.tmp"),
"contents": "this is an update test",
"mode": defaultPerm,
"directory": defaultDirectory,
"name": "update_basic.tmp",
"contents": "this is an update test",
},
filepath.Join(defaultDirectory, "update_basic.tmp"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
setup(tc.setup)
defer teardown(tc.tearDownPath)
if err := tc.fit.client.Create(tc.setup["directory"], tc.setup["name"], tc.setup["contents"], tc.setup["mode"]); err != nil {
t.Errorf("Error setting up: %v", err)
}
defer func() {
if err := tc.fit.client.Delete(tc.setup["directory"], tc.setup["name"]); err != nil {
t.Errorf("Error tearing down: %v", err)
}
}()
r := getUpdateResponseContainer()
tc.fit.Update(context.Background(), tc.have, &r)
got := r
@ -392,13 +402,12 @@ func TestLocalResourceUpdate(t *testing.T) {
t.Errorf("Failed to get planned state: %v", diags)
}
plannedContents := plannedState.Contents.ValueString()
plannedFilePath := filepath.Join(plannedState.Directory.ValueString(), plannedState.Name.ValueString())
contentsAfterUpdate, err := os.ReadFile(plannedFilePath)
_, contentsAfterUpdate, err := tc.fit.client.Read(plannedState.Directory.ValueString(), plannedState.Name.ValueString())
if err != nil {
t.Errorf("Failed to read file for update verification: %s", err)
}
if string(contentsAfterUpdate) != plannedContents {
t.Errorf("File content was not updated correctly. Got %q, want %q", string(contentsAfterUpdate), plannedContents)
if contentsAfterUpdate != plannedContents {
t.Errorf("File content was not updated correctly. Got %q, want %q", contentsAfterUpdate, plannedContents)
}
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("Update() mismatch (-want +got):\n%s", diff)
@ -411,22 +420,21 @@ func TestLocalResourceUpdate(t *testing.T) {
func TestLocalResourceDelete(t *testing.T) {
t.Run("Delete function", func(t *testing.T) {
testCases := []struct {
name string
fit LocalResource
have resource.DeleteRequest
want resource.DeleteResponse
setup map[string]string
tearDownPath string
name string
fit LocalResource
have resource.DeleteRequest
want resource.DeleteResponse
setup map[string]string
}{
{
"Basic test",
LocalResource{},
LocalResource{client: &memoryFileClient{}},
// have
getDeleteRequest(t, map[string]string{
"id": "fd6fb8621c4850c228190f4d448ce30881a32609d6b4c7341d48d0027e597567",
"name": "delete.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"permissions": defaultPerm,
"contents": "this is a delete test",
"protected": defaultProtected,
"hmac_secret_key": defaultHmacSecretKey,
@ -435,22 +443,27 @@ func TestLocalResourceDelete(t *testing.T) {
getDeleteResponse(),
// setup
map[string]string{
"mode": defaultMode,
"path": filepath.Join(defaultDirectory, "delete.tmp"),
"contents": "this is a delete test",
"mode": defaultPerm,
"directory": defaultDirectory,
"name": "delete.tmp",
"contents": "this is a delete test",
},
filepath.Join(defaultDirectory, "delete.tmp"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
setup(tc.setup)
if err := tc.fit.client.Create(tc.setup["directory"], tc.setup["name"], tc.setup["contents"], tc.setup["mode"]); err != nil {
t.Errorf("Error setting up: %v", err)
}
r := getDeleteResponseContainer()
tc.fit.Delete(context.Background(), tc.have, &r)
got := r
// Verify the file was actually deleted from disk
if _, err := os.Stat(tc.setup["path"]); !os.IsNotExist(err) {
t.Errorf("Expected file to be deleted, but it still exists.")
if _, c, err := tc.fit.client.Read(tc.setup["directory"], tc.setup["name"]); err == nil || err.Error() != "file not found" {
if err == nil {
t.Errorf("Expected file to be delete, but it still exists. File contents: %s", c)
}
t.Errorf("Expected file to be deleted, but it still exists. Error: %s", err.Error())
}
// verify that the file was removed from state
if diff := cmp.Diff(tc.want, got); diff != "" {
@ -682,7 +695,7 @@ func getObjectAttributeTypes() tftypes.Object {
"id": tftypes.String,
"name": tftypes.String,
"directory": tftypes.String,
"mode": tftypes.String,
"permissions": tftypes.String,
"contents": tftypes.String,
"hmac_secret_key": tftypes.String,
"protected": tftypes.Bool,
@ -697,11 +710,42 @@ func getLocalResourceSchema() *resource.SchemaResponse {
return r
}
func setup(data map[string]string) {
modeInt, _ := strconv.ParseUint(data["mode"], 8, 32)
_ = os.WriteFile(data["path"], []byte(data["contents"]), os.FileMode(modeInt))
// type fileClient interface {
// Create(directory string, name string, data string, permissions string) error
// // If file isn't found the error message must have err.Error() == "File not found."
// Read(directory string, name string) (string, string, error)// permissions, contents, error
// Update(currentDirectory string, currentName string, newDirectory string, newName string, data string, permissions string) error
// Delete(directory string, name string) error
// }
type memoryFileClient struct {
file map[string]string
}
func teardown(path string) {
os.Remove(path)
var _ fileClient = &memoryFileClient{} // make sure the memoryFileClient implements the fileClient
func (c *memoryFileClient) Create(directory string, name string, data string, permissions string) error {
c.file = make(map[string]string)
c.file["directory"] = directory
c.file["name"] = name
c.file["contents"] = data
c.file["permissions"] = permissions
return nil
}
func (c *memoryFileClient) Read(directory string, name string) (string, string, error) {
if c.file["directory"] == "" || c.file["name"] == "" {
return "", "", fmt.Errorf("file not found")
}
return c.file["permissions"], c.file["contents"], nil
}
func (c *memoryFileClient) Update(currentDirectory string, currentName string, newDirectory string, newName string, data string, permissions string) error {
c.file["directory"] = newDirectory
c.file["name"] = newName
c.file["contents"] = data
c.file["permissions"] = permissions
return nil
}
func (c *memoryFileClient) Delete(directory string, name string) error {
c.file = nil
return nil
}

View File

@ -30,7 +30,6 @@ require (
github.com/go-openapi/errors v0.20.2 // indirect
github.com/go-openapi/strfmt v0.21.3 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-github/v45 v45.2.0 // indirect
github.com/google/go-github/v53 v53.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
@ -82,13 +81,11 @@ require (
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/oauth2 v0.27.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@ -147,7 +147,6 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
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.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@ -504,8 +503,9 @@ golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
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-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
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=
@ -609,7 +609,6 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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.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-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@ -637,8 +636,6 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
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.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=