Skip to content

Commit

Permalink
[TF-7941] Add support for Terraform Test (#755)
Browse files Browse the repository at this point in the history
* [WIP][TF-7941] Add support for Terraform Test

* try and fix lint problems

* generate_mocks

* add more tests + beta docs

* missing beta docs

* waitUntiTestRunStatus docs

* Apply suggestions from code review

Co-authored-by: Brandon Croft <brandon.croft@gmail.com>

* address comments

* lint

---------

Co-authored-by: Liam Cervante <liam.cervante@hashicorp.com>
Co-authored-by: Brandon Croft <brandon.croft@gmail.com>
  • Loading branch information
3 people authored Sep 11, 2023
1 parent 6d1fc7a commit c69ce29
Show file tree
Hide file tree
Showing 15 changed files with 1,075 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ This API client covers most of the existing Terraform Cloud API calls and is upd
- [x] Team Membership
- [x] Team Tokens
- [x] Teams
- [x] Test Runs
- [x] User Tokens
- [x] Users
- [x] Variable Sets
Expand Down
27 changes: 27 additions & 0 deletions configuration_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ type ConfigurationVersions interface {
// configuration version will be usable once data is uploaded to it.
Create(ctx context.Context, workspaceID string, options ConfigurationVersionCreateOptions) (*ConfigurationVersion, error)

// CreateForRegistryModule is used to create a new configuration version
// keyed to a registry module instead of a workspace. The created
// configuration version will be usable once data is uploaded to it.
//
// **Note: This function is still in BETA and subject to change.**
CreateForRegistryModule(ctx context.Context, moduleID RegistryModuleID) (*ConfigurationVersion, error)

// Read a configuration version by its ID.
Read(ctx context.Context, cvID string) (*ConfigurationVersion, error)

Expand Down Expand Up @@ -232,6 +239,26 @@ func (s *configurationVersions) Create(ctx context.Context, workspaceID string,
return cv, nil
}

func (s *configurationVersions) CreateForRegistryModule(ctx context.Context, moduleID RegistryModuleID) (*ConfigurationVersion, error) {
if err := moduleID.valid(); err != nil {
return nil, err
}

u := fmt.Sprintf("%s/configuration-versions", testRunsPath(moduleID))
req, err := s.client.NewRequest("POST", u, nil)
if err != nil {
return nil, err
}

cv := &ConfigurationVersion{}
err = req.Do(ctx, cv)
if err != nil {
return nil, err
}

return cv, nil
}

// Read a configuration version by its ID.
func (s *configurationVersions) Read(ctx context.Context, cvID string) (*ConfigurationVersion, error) {
return s.ReadWithOptions(ctx, cvID, nil)
Expand Down
50 changes: 50 additions & 0 deletions configuration_version_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,56 @@ func TestConfigurationVersionsCreate(t *testing.T) {
})
}

func TestConfigurationVersionsCreateForRegistryModule(t *testing.T) {
skipUnlessBeta(t)
client := testClient(t)
ctx := context.Background()

orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()

rmTest, rmTestCleanup := createRegistryModule(t, client, orgTest, PrivateRegistry)
defer rmTestCleanup()

id := RegistryModuleID{
Organization: rmTest.Organization.Name,
Name: rmTest.Name,
Provider: rmTest.Provider,
Namespace: rmTest.Namespace,
RegistryName: rmTest.RegistryName,
}

t.Run("with valid options", func(t *testing.T) {
cv, err := client.ConfigurationVersions.CreateForRegistryModule(ctx, id)
assert.NotEmpty(t, cv.UploadURL)
require.NoError(t, err)

// Get a refreshed view of the configuration version.
refreshed, err := client.ConfigurationVersions.Read(ctx, cv.ID)
require.NoError(t, err)
assert.Empty(t, refreshed.UploadURL)

for _, item := range []*ConfigurationVersion{
cv,
refreshed,
} {
assert.NotEmpty(t, item.ID)
assert.Empty(t, item.Error)
assert.Equal(t, item.Source, ConfigurationSourceAPI)
assert.Equal(t, item.Status, ConfigurationPending)
}
})

t.Run("with invalid workspace id", func(t *testing.T) {
cv, err := client.ConfigurationVersions.CreateForRegistryModule(
ctx,
RegistryModuleID{},
)
assert.Nil(t, cv)
assert.Equal(t, ErrRequiredName, err)
})
}

func TestConfigurationVersionsRead(t *testing.T) {
client := testClient(t)
ctx := context.Background()
Expand Down
2 changes: 2 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,8 @@ var (

ErrRequiredRegistryModule = errors.New("registry module is required")

ErrInvalidTestRunID = errors.New("invalid value for test run id")

ErrTerraformVersionValidForPlanOnly = errors.New("setting terraform-version is only valid when plan-only is set to true")

ErrStateMustBeOmitted = errors.New("when uploading state, the State and JSONState strings must be omitted from options")
Expand Down
1 change: 1 addition & 0 deletions generate_mocks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ mockgen -source=team_access.go -destination=mocks/team_access_mocks.go -package=
mockgen -source=team_member.go -destination=mocks/team_member_mocks.go -package=mocks
mockgen -source=team_project_access.go -destination=mocks/team_project_access_mocks.go -package=mocks
mockgen -source=team_token.go -destination=mocks/team_token_mocks.go -package=mocks
mockgen -source=test_run.go -destination=mocks/test_run_mocks.go -package=mocks
mockgen -source=user.go -destination=mocks/user_mocks.go -package=mocks
mockgen -source=user_token.go -destination=mocks/user_token_mocks.go -package=mocks
mockgen -source=variable.go -destination=mocks/variable_mocks.go -package=mocks
Expand Down
147 changes: 147 additions & 0 deletions helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,49 @@ func createUploadedConfigurationVersion(t *testing.T, client *Client, w *Workspa
return cv, cvCleanup
}

func createTestRunConfigurationVersion(t *testing.T, client *Client, rm *RegistryModule) (*ConfigurationVersion, func()) {
var rmCleanup func()

if rm == nil {
rm, rmCleanup = createRegistryModuleWithVersion(t, client, nil)
}

ctx := context.Background()
cv, err := client.ConfigurationVersions.CreateForRegistryModule(
ctx,
RegistryModuleID{
Organization: rm.Organization.Name,
Name: rm.Name,
Provider: rm.Provider,
Namespace: rm.Namespace,
RegistryName: rm.RegistryName,
})
if err != nil {
t.Fatal(err)
}

return cv, func() {
if rmCleanup != nil {
rmCleanup()
}
}
}

func createUploadedTestRunConfigurationVersion(t *testing.T, client *Client, rm *RegistryModule) (*ConfigurationVersion, func()) {
cv, cvCleanup := createTestRunConfigurationVersion(t, client, rm)

ctx := context.Background()
err := client.ConfigurationVersions.Upload(ctx, cv.UploadURL, "test-fixtures/config-version-with-test")
if err != nil {
cvCleanup()
t.Fatal(err)
}

WaitUntilStatus(t, client, cv, ConfigurationUploaded, 15)

return cv, cvCleanup
}

// helper to wait until a configuration version has reached a certain status
func WaitUntilStatus(t *testing.T, client *Client, cv *ConfigurationVersion, desiredStatus ConfigurationStatus, timeoutSeconds int) {
ctx := context.Background()
Expand Down Expand Up @@ -1268,6 +1311,54 @@ func createRun(t *testing.T, client *Client, w *Workspace) (*Run, func()) {
}
}

func createTestRun(t *testing.T, client *Client, rm *RegistryModule, variables ...*RunVariable) (*TestRun, func()) {
var rmCleanup func()

if rm == nil {
rm, rmCleanup = createBranchBasedRegistryModule(t, client, nil)
}

cv, cvCleanup := createUploadedTestRunConfigurationVersion(t, client, rm)

ctx := context.Background()
tr, err := client.TestRuns.Create(ctx, TestRunCreateOptions{
Variables: variables,
ConfigurationVersion: cv,
RegistryModule: rm,
})
if err != nil {
t.Fatal(err)
}

return tr, func() {
cvCleanup()

if rmCleanup != nil {
rmCleanup()
}
}
}

// helper to wait until a test run has reached a certain status
func waitUntilTestRunStatus(t *testing.T, client *Client, rm RegistryModuleID, tr *TestRun, desiredStatus TestRunStatus, timeoutSeconds int) {
ctx := context.Background()

for i := 0; ; i++ {
refreshed, err := client.TestRuns.Read(ctx, rm, tr.ID)
require.NoError(t, err)

if refreshed.Status == desiredStatus {
break
}

if i > timeoutSeconds {
t.Fatalf("Timeout waiting for the test run status %s", string(desiredStatus))
}

time.Sleep(1 * time.Second)
}
}

func createPlanExport(t *testing.T, client *Client, r *Run) (*PlanExport, func()) {
var rCleanup func()

Expand Down Expand Up @@ -1319,6 +1410,62 @@ func createPlanExport(t *testing.T, client *Client, r *Run) (*PlanExport, func()
}
}

func createBranchBasedRegistryModule(t *testing.T, client *Client, org *Organization) (*RegistryModule, func()) {
githubIdentifier := os.Getenv("GITHUB_REGISTRY_MODULE_IDENTIFIER")
if githubIdentifier == "" {
t.Skip("Export a valid GITHUB_REGISTRY_MODULE_IDENTIFIER before running this test")
}

githubBranch := os.Getenv("GITHUB_REGISTRY_MODULE_BRANCH")
if githubBranch == "" {
githubBranch = "main"
}

var orgCleanup func()
if org == nil {
org, orgCleanup = createOrganization(t, client)
}

oauthTokenTest, oauthTokenTestCleanup := createOAuthToken(t, client, org)

ctx := context.Background()

rm, err := client.RegistryModules.CreateWithVCSConnection(ctx, RegistryModuleCreateWithVCSConnectionOptions{
VCSRepo: &RegistryModuleVCSRepoOptions{
OrganizationName: String(org.Name),
Identifier: String(githubIdentifier),
OAuthTokenID: String(oauthTokenTest.ID),
DisplayIdentifier: String(githubIdentifier),
Branch: String(githubBranch),
},
InitialVersion: String("1.0.0"),
})

if err != nil {
oauthTokenTestCleanup()

if orgCleanup != nil {
orgCleanup()
}

t.Fatal(err)
}

return rm, func() {
if err := client.RegistryModules.Delete(ctx, org.Name, rm.Name); err != nil {
t.Errorf("Error destroying registry module! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"Registry Module: %s\nError: %s", rm.Name, err)
}

oauthTokenTestCleanup()

if orgCleanup != nil {
orgCleanup()
}
}
}

func createRegistryModule(t *testing.T, client *Client, org *Organization, registryName RegistryName) (*RegistryModule, func()) {
var orgCleanup func()

Expand Down
15 changes: 15 additions & 0 deletions mocks/configuration_version_mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit c69ce29

Please sign in to comment.