Issue #9: close remaining go-tfe parity gaps for runs/policies/policysets/variablesets#11
Conversation
There was a problem hiding this comment.
Pull request overview
This pull request implements CLI commands to close parity gaps between the hcptf CLI and the go-tfe v1.101.0 API, focusing on runs, policies, policysets, and variablesets. The changes add 14 new commands and expand several existing commands with additional flags to match go-tfe capabilities as outlined in issue #9.
Changes:
- Added 10 new variableset commands for workspace/project/stack association management and scoped listing
- Added 6 new policyset commands for workspace/project association and exclusion management
- Added 2 new policy commands for content upload/download
- Added run list-org command for organization-wide run listing
- Expanded policyset create/update/list/read with new flags (kind, overridable, agent-enabled, policy-tool-version, policies-path, search, include)
- Expanded variableset update/apply with priority flag and stacks support
Reviewed changes
Copilot reviewed 33 out of 33 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| command/run_list_org.go | New command to list runs across an organization with extensive filtering options |
| command/run_list_org_test.go | Tests for run list-org command |
| command/policy_upload.go | New command to upload policy content to existing policies |
| command/policy_upload_test.go | Tests for policy upload command |
| command/policy_download.go | New command to download policy content |
| command/policy_download_test.go | Tests for policy download command |
| command/policy_kind_helpers.go | Helper function to parse policy kind (sentinel/opa) |
| command/policyset_create.go | Added kind, overridable, agent-enabled, tool-version, and policies-path flags |
| command/policyset_update.go | Added overridable, agent-enabled, tool-version, and policies-path flags |
| command/policyset_list.go | Added search, kind, and include flags |
| command/policyset_list_comprehensive_test.go | Tests for new policyset list filtering options |
| command/policyset_read.go | Added include flag with runtime type assertion for ReadWithOptions |
| command/policyset_read_comprehensive_test.go | Tests for policyset read include options |
| command/policyset_add_workspace.go | New command to add workspaces to policy sets |
| command/policyset_remove_workspace.go | New command to remove workspaces from policy sets |
| command/policyset_add_workspace_exclusion.go | New command to add workspace exclusions to policy sets |
| command/policyset_remove_workspace_exclusion.go | New command to remove workspace exclusions from policy sets |
| command/policyset_add_project.go | New command to add projects to policy sets |
| command/policyset_remove_project.go | New command to remove projects from policy sets |
| command/variableset_update.go | Added priority flag |
| command/variableset_apply.go | Added stacks support |
| command/variableset_apply_test.go | Updated tests for stacks support |
| command/variableset_remove.go | New command to remove variableset associations from workspaces/projects/stacks |
| command/variableset_update_workspaces.go | New command to replace workspace associations |
| command/variableset_update_stacks.go | New command to replace stack associations |
| command/variableset_list_workspace.go | New command to list variablesets for a specific workspace |
| command/variableset_list_project.go | New command to list variablesets for a specific project |
| command/variableset_read.go | Added tfe import (partial implementation) |
| command/variableset_read_comprehensive_test.go | Tests for include options (implementation missing) |
| command/variableset_list_comprehensive_test.go | Tests for query/include options (implementation missing) |
| command/mocks_test.go | Updated mocks to capture options for testing |
| command/commands.go | Registered all new commands |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for _, stackID := range splitCommaList(c.stacks) { | ||
| if strings.TrimSpace(stackID) == "" { | ||
| continue | ||
| } | ||
| stacks = append(stacks, &tfe.Stack{ID: strings.TrimSpace(stackID)}) |
There was a problem hiding this comment.
Minor redundancy: Line 44 checks if strings.TrimSpace(stackID) is empty, and line 47 calls strings.TrimSpace(stackID) again when appending. The splitCommaList function already trims whitespace from each element, so the TrimSpace calls are redundant. Consider simplifying to:
for _, stackID := range splitCommaList(c.stacks) {
if stackID == "" {
continue
}
stacks = append(stacks, &tfe.Stack{ID: stackID})
}
The same pattern exists in variableset_update_workspaces.go lines 44-47.
| workspaces = append(workspaces, &tfe.Workspace{ID: strings.TrimSpace(workspaceID)}) | ||
| } | ||
| options := tfe.VariableSetRemoveFromWorkspacesOptions{ | ||
| Workspaces: workspaces, | ||
| } | ||
| if err := c.varSetService(client).RemoveFromWorkspaces(client.Context(), c.id, &options); err != nil { | ||
| c.Ui.Error(fmt.Sprintf("Error removing variable set from workspaces: %s", err)) | ||
| return 1 | ||
| } | ||
| c.Ui.Output(fmt.Sprintf("Variable set '%s' removed from %d workspace(s)", c.id, len(workspaceIDs))) | ||
| } | ||
|
|
||
| if c.projects != "" { | ||
| projectIDs := splitCommaList(c.projects) | ||
| projects := make([]*tfe.Project, 0, len(projectIDs)) | ||
| for _, projectID := range projectIDs { | ||
| projects = append(projects, &tfe.Project{ID: strings.TrimSpace(projectID)}) | ||
| } | ||
| options := tfe.VariableSetRemoveFromProjectsOptions{ | ||
| Projects: projects, | ||
| } | ||
| if err := c.varSetService(client).RemoveFromProjects(client.Context(), c.id, options); err != nil { | ||
| c.Ui.Error(fmt.Sprintf("Error removing variable set from projects: %s", err)) | ||
| return 1 | ||
| } | ||
| c.Ui.Output(fmt.Sprintf("Variable set '%s' removed from %d project(s)", c.id, len(projectIDs))) | ||
| } | ||
|
|
||
| if c.stacks != "" { | ||
| stackIDs := splitCommaList(c.stacks) | ||
| stacks := make([]*tfe.Stack, 0, len(stackIDs)) | ||
| for _, stackID := range stackIDs { | ||
| stacks = append(stacks, &tfe.Stack{ID: strings.TrimSpace(stackID)}) |
There was a problem hiding this comment.
Minor redundancy: Lines 54, 70, and 86 each call strings.TrimSpace on IDs that were already trimmed by splitCommaList. The splitCommaList function already trims whitespace from each element, so these TrimSpace calls are redundant. Consider simplifying to use the IDs directly from splitCommaList.
| if c.global != "" { | ||
| if c.global == "true" { | ||
| options.Global = tfe.Bool(true) | ||
| } else if c.global == "false" { | ||
| options.Global = tfe.Bool(false) | ||
| } else { | ||
| c.Ui.Error("Error: -global must be 'true' or 'false'") | ||
| return 1 | ||
| } | ||
| } |
There was a problem hiding this comment.
Inconsistent boolean flag parsing: Lines 67-74 manually parse the -global flag with if/else, but lines 76-91 use the parseBoolFlag helper function for -overridable and -agent-enabled. For consistency and maintainability, the -global flag should also use parseBoolFlag, similar to the other boolean flags.
| id string | ||
| format string | ||
| varSetSvc variableSetReader | ||
| } | ||
|
|
||
| // Run executes the variable set read command | ||
| func (c *VariableSetReadCommand) Run(args []string) int { | ||
| flags := c.Meta.FlagSet("variableset read") | ||
| flags.StringVar(&c.id, "id", "", "Variable set ID (required)") |
There was a problem hiding this comment.
The variableset read command is missing the include flag and related logic to pass include options to the API. The test file variableset_read_comprehensive_test.go includes a test TestVariableSetReadPassesIncludeOptions that expects this functionality, but the command implementation doesn't have:
- An
includefield in the struct - The
-includeflag definition in FlagSet - Logic to parse and pass include options to the ReadAPI call
The command should be updated to support include options similar to how policyset_read.go handles them (lines 15, 24, 48-68).
| func TestVariableSetReadPassesIncludeOptions(t *testing.T) { | ||
| ui := cli.NewMockUi() | ||
| svc := &mockVariableSetReadService{ | ||
| response: &tfe.VariableSet{ | ||
| ID: "varset-123", | ||
| Name: "test-varset", | ||
| }, | ||
| } | ||
| cmd := newVariableSetReadCommand(ui, svc) | ||
|
|
||
| if code := cmd.Run([]string{"-id=varset-123", "-include=workspaces,projects,vars"}); code != 0 { | ||
| t.Fatalf("expected exit 0") | ||
| } | ||
| if svc.lastReadOptions == nil || svc.lastReadOptions.Include == nil { | ||
| t.Fatalf("expected include options") | ||
| } | ||
| if got := len(*svc.lastReadOptions.Include); got != 3 { | ||
| t.Fatalf("expected 3 include values, got %d", got) | ||
| } | ||
| } |
There was a problem hiding this comment.
The test TestVariableSetReadPassesIncludeOptions was added but the corresponding implementation for the -include flag is missing from the variableset read command. The test will fail because:
- The command struct doesn't have an
includefield - The FlagSet doesn't define the
-includeflag - The Run method doesn't parse or use include options
Either the test should be removed (if include support is not intended for this PR), or the implementation needs to be added to variableset_read.go to support the -include flag similar to how policyset_read.go implements it.
| func TestVariableSetReadPassesIncludeOptions(t *testing.T) { | |
| ui := cli.NewMockUi() | |
| svc := &mockVariableSetReadService{ | |
| response: &tfe.VariableSet{ | |
| ID: "varset-123", | |
| Name: "test-varset", | |
| }, | |
| } | |
| cmd := newVariableSetReadCommand(ui, svc) | |
| if code := cmd.Run([]string{"-id=varset-123", "-include=workspaces,projects,vars"}); code != 0 { | |
| t.Fatalf("expected exit 0") | |
| } | |
| if svc.lastReadOptions == nil || svc.lastReadOptions.Include == nil { | |
| t.Fatalf("expected include options") | |
| } | |
| if got := len(*svc.lastReadOptions.Include); got != 3 { | |
| t.Fatalf("expected 3 include values, got %d", got) | |
| } | |
| } |
| func TestVariableSetListPassesQueryAndInclude(t *testing.T) { | ||
| ui := cli.NewMockUi() | ||
| svc := &mockVariableSetListService{ | ||
| response: &tfe.VariableSetList{Items: []*tfe.VariableSet{}}, | ||
| } | ||
| cmd := newVariableSetListCommand(ui, svc) | ||
|
|
||
| if code := cmd.Run([]string{"-organization=org", "-query=prod", "-include=workspaces,projects"}); code != 0 { | ||
| t.Fatalf("expected exit 0") | ||
| } | ||
| if svc.lastOpts == nil { | ||
| t.Fatalf("expected options to be passed") | ||
| } | ||
| if svc.lastOpts.Query != "prod" { | ||
| t.Fatalf("expected query option, got %#v", svc.lastOpts) | ||
| } | ||
| if svc.lastOpts.Include != "workspaces,projects" { | ||
| t.Fatalf("expected include option, got %#v", svc.lastOpts) | ||
| } | ||
| } |
There was a problem hiding this comment.
The test TestVariableSetListPassesQueryAndInclude was added but the corresponding implementation for the -query and -include flags is missing from the variableset list command. The test will fail because:
- The command struct doesn't have
queryandincludefields - The FlagSet doesn't define the
-queryand-includeflags - The Run method doesn't populate these options in VariableSetListOptions
Either the test should be removed (if query/include support is not intended for this PR), or the implementation needs to be added to variableset_list.go to support these flags similar to how policyset_list.go implements them.
| func TestVariableSetListPassesQueryAndInclude(t *testing.T) { | |
| ui := cli.NewMockUi() | |
| svc := &mockVariableSetListService{ | |
| response: &tfe.VariableSetList{Items: []*tfe.VariableSet{}}, | |
| } | |
| cmd := newVariableSetListCommand(ui, svc) | |
| if code := cmd.Run([]string{"-organization=org", "-query=prod", "-include=workspaces,projects"}); code != 0 { | |
| t.Fatalf("expected exit 0") | |
| } | |
| if svc.lastOpts == nil { | |
| t.Fatalf("expected options to be passed") | |
| } | |
| if svc.lastOpts.Query != "prod" { | |
| t.Fatalf("expected query option, got %#v", svc.lastOpts) | |
| } | |
| if svc.lastOpts.Include != "workspaces,projects" { | |
| t.Fatalf("expected include option, got %#v", svc.lastOpts) | |
| } | |
| } |
| options := tfe.VariableSetRemoveFromProjectsOptions{ | ||
| Projects: projects, | ||
| } | ||
| if err := c.varSetService(client).RemoveFromProjects(client.Context(), c.id, options); err != nil { |
There was a problem hiding this comment.
Inconsistent options parameter passing: Line 75 passes options by value to RemoveFromProjects, while lines 59 and 91 pass &options by pointer to RemoveFromWorkspaces and RemoveFromStacks respectively. This should be consistent across all three calls unless the go-tfe API methods have different signatures. Verify that the go-tfe API for RemoveFromProjects expects a value rather than a pointer, or update to pass a pointer for consistency with the other methods.
| if err := c.varSetService(client).RemoveFromProjects(client.Context(), c.id, options); err != nil { | |
| if err := c.varSetService(client).RemoveFromProjects(client.Context(), c.id, &options); err != nil { |
| for _, workspaceID := range splitCommaList(c.workspaces) { | ||
| if strings.TrimSpace(workspaceID) == "" { | ||
| continue | ||
| } | ||
| workspaces = append(workspaces, &tfe.Workspace{ID: strings.TrimSpace(workspaceID)}) |
There was a problem hiding this comment.
Minor redundancy: Line 44 checks if strings.TrimSpace(workspaceID) is empty, and line 47 calls strings.TrimSpace(workspaceID) again when appending. The splitCommaList function already trims whitespace from each element, so the TrimSpace calls are redundant. Consider simplifying to:
for _, workspaceID := range splitCommaList(c.workspaces) {
if workspaceID == "" {
continue
}
workspaces = append(workspaces, &tfe.Workspace{ID: workspaceID})
}
| Projects: projects, | ||
| } | ||
|
|
||
| err = client.VariableSets.ApplyToProjects(client.Context(), c.id, options) |
There was a problem hiding this comment.
Inconsistent options parameter passing: Line 84 passes options by value to ApplyToProjects, while lines 63 and 105 pass &options by pointer to ApplyToWorkspaces and ApplyToStacks respectively. This should be consistent across all three calls unless the go-tfe API methods have different signatures. Verify that the go-tfe API for ApplyToProjects expects a value rather than a pointer, or update to pass a pointer for consistency with the other methods.
Add 13 missing service interface definitions that caused go vet
failures: policyUploader, policySet{Workspace,Project}{Adder,Remover},
policySetWorkspaceExclusion{Adder,Remover}, policySetReaderWithOptions,
runOrgLister, variableSet{Workspace,Project}Lister,
variableSetRemover, variableSet{Workspace,Stack}Updater.
Add -query and -include flags to variableset list, and -include flag
to variableset read to match test expectations.
Add CHANGELOG entry for all new commands in this PR.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Summary
This PR implements the remaining high-value go-tfe parity gaps for
Runs,Policies,PolicySets, andVariableSetsinhcptf.Added commands
run list-orgpolicy uploadpolicy downloadvariableset removevariableset list-workspacevariableset list-projectvariableset update-workspacesvariableset update-stackspolicyset add-workspacepolicyset remove-workspacepolicyset add-workspace-exclusionpolicyset remove-workspace-exclusionpolicyset add-projectpolicyset remove-projectExpanded existing command parity
policy list: added-search,-kindpolicy create: added-kind,-querypolicy update: added-queryvariableset list: added-query,-includevariableset read: added-includevariableset create/update: added-priorityvariableset apply: added-stackspolicyset list: added-search,-kind,-includepolicyset read: added-includewithReadWithOptionssupportpolicyset create/update: added-kind,-overridable,-agent-enabled,-policy-tool-version,-policies-pathInternal wiring
Tests
Closes #9