Skip to content

Commit

Permalink
Feat: add new option to template which populates variables from sourc… (
Browse files Browse the repository at this point in the history
#393)

* Feat: add new option to template which populates variables from source code

* added custom writer

* update integration test
  • Loading branch information
TomerHeber committed Jun 8, 2022
1 parent 4ee860f commit f1a1a2b
Show file tree
Hide file tree
Showing 14 changed files with 403 additions and 5 deletions.
1 change: 1 addition & 0 deletions client/api_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type ApiClientInterface interface {
TemplateDelete(id string) error
AssignTemplateToProject(id string, payload TemplateAssignmentToProjectPayload) (Template, error)
RemoveTemplateFromProject(templateId string, projectId string) error
VariablesFromRepository(payload *VariablesFromRepositoryPayload) ([]ConfigurationVariable, error)
SshKeys() ([]SshKey, error)
SshKeyCreate(payload SshKeyCreatePayload) (*SshKey, error)
SshKeyDelete(id string) error
Expand Down
15 changes: 15 additions & 0 deletions client/api_client_mock.go

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

7 changes: 7 additions & 0 deletions client/configuration_variable.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ type ConfigurationVariableSchema struct {
Format Format `json:"format,omitempty"`
}

func (c *ConfigurationVariableSchema) ResourceDataSliceStructValueWrite(values map[string]interface{}) error {
if len(c.Format) > 0 {
values["format"] = c.Format
}
return nil
}

type ConfigurationVariable struct {
ScopeId string `json:"scopeId,omitempty"`
Value string `json:"value"`
Expand Down
43 changes: 43 additions & 0 deletions client/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package client

import (
"errors"
"strconv"
"strings"
)

type TemplateRetryOn struct {
Expand Down Expand Up @@ -93,6 +95,16 @@ type TemplateAssignmentToProject struct {
ProjectId string `json:"projectId"`
}

type VariablesFromRepositoryPayload struct {
BitbucketClientKey string `json:"bitbucketClientKey,omitempty"`
GithubInstallationId int `json:"githubInstallationId,omitempty"`
Path string `json:"path"`
Revision string `json:"revision"`
SshKeyIds []string `json:"sshKeyIds"`
TokenId string `json:"tokenId,omitempty"`
Repository string `json:"repository"`
}

func (client *ApiClient) TemplateCreate(payload TemplateCreatePayload) (Template, error) {
if payload.Name == "" {
return Template{}, errors.New("must specify template name on creation")
Expand Down Expand Up @@ -188,3 +200,34 @@ func (client *ApiClient) AssignTemplateToProject(id string, payload TemplateAssi
func (client *ApiClient) RemoveTemplateFromProject(templateId string, projectId string) error {
return client.http.Delete("/blueprints/" + templateId + "/projects/" + projectId)
}

func (client *ApiClient) VariablesFromRepository(payload *VariablesFromRepositoryPayload) ([]ConfigurationVariable, error) {
paramsInterface, err := toParamsInterface(payload)
if err != nil {
return nil, err
}

params := map[string]string{}
for key, value := range paramsInterface {
if key == "githubInstallationId" {
params[key] = strconv.Itoa(int(value.(float64)))
} else if key == "sshKeyIds" {
sshkeys := []string{}
if value != nil {
for _, sshkey := range value.([]interface{}) {
sshkeys = append(sshkeys, "\""+sshkey.(string)+"\"")
}
}
params[key] = "[" + strings.Join(sshkeys, ",") + "]"
} else {
params[key] = value.(string)
}
}

var result []ConfigurationVariable
if err := client.http.Get("/blueprints/variables-from-repository", params, &result); err != nil {
return nil, err
}

return result, nil
}
38 changes: 38 additions & 0 deletions client/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package client_test
import (
"encoding/json"
"errors"
"strconv"

. "github.com/env0/terraform-provider-env0/client"
"github.com/golang/mock/gomock"
Expand Down Expand Up @@ -271,4 +272,41 @@ var _ = Describe("Templates Client", func() {
})
})
})

Describe("VariablesFromRepository", func() {
var returnedVariables []ConfigurationVariable
var err error

payload := &VariablesFromRepositoryPayload{
GithubInstallationId: 1111,
Path: "path",
Revision: "1",
Repository: "main",
SshKeyIds: []string{
"1", "2",
},
}

expectedParams := map[string]string{
"githubInstallationId": strconv.Itoa(payload.GithubInstallationId),
"path": payload.Path,
"revision": payload.Revision,
"repository": payload.Repository,
"sshKeyIds": `["1","2"]`,
}

BeforeEach(func() {
httpCall = mockHttpClient.EXPECT().
Get("/blueprints/variables-from-repository", expectedParams, gomock.Any()).
Do(func(path string, request interface{}, response *[]ConfigurationVariable) {
*response = []ConfigurationVariable{}
})
returnedVariables, err = apiClient.VariablesFromRepository(payload)
})

It("Should return variables", func() {
Expect(err).To(BeNil())
Expect(returnedVariables).To(Equal([]ConfigurationVariable{}))
})
})
})
17 changes: 17 additions & 0 deletions client/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package client

import "encoding/json"

func toParamsInterface(i interface{}) (map[string]interface{}, error) {
b, err := json.Marshal(i)
if err != nil {
return nil, err
}

var paramsInterface map[string]interface{}
if err := json.Unmarshal(b, &paramsInterface); err != nil {
return nil, err
}

return paramsInterface, nil
}
81 changes: 81 additions & 0 deletions env0/data_source_code_variables.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package env0

import (
"context"

"github.com/env0/terraform-provider-env0/client"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func dataSourceCodeVariables() *schema.Resource {
return &schema.Resource{
ReadContext: dataSourceCodeVariablesRead,

Schema: map[string]*schema.Schema{
"template_id": {
Type: schema.TypeString,
Description: "extracts source code terraform variables from the VCS configuration of this template",
Required: true,
},

"variables": {
Type: schema.TypeList,
Description: "a list of terraform variables extracted from the source code",
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Description: "the name of the terraform variable",
Computed: true,
},
"value": {
Type: schema.TypeString,
Description: "the value of the terraform variable",
Computed: true,
},
"format": {
Type: schema.TypeString,
Description: "the format of the terraform variable (HCL or JSON)",
Computed: true,
},
},
},
},
},
}
}

func dataSourceCodeVariablesRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
apiClient := meta.(client.ApiClientInterface)

templateId := d.Get("template_id").(string)

template, err := apiClient.Template(templateId)
if err != nil {
return diag.Errorf("could not get template: %v", err)
}

payload := &client.VariablesFromRepositoryPayload{
BitbucketClientKey: template.BitbucketClientKey,
GithubInstallationId: template.GithubInstallationId,
Path: template.Path,
Revision: template.Revision,
TokenId: template.TokenId,
Repository: template.Repository,
}

variables, err := apiClient.VariablesFromRepository(payload)
if err != nil {
return diag.Errorf("failed to extract variables from repository: %v", err)
}

if err := writeResourceDataSlice(variables, "variables", d); err != nil {
return diag.Errorf("schema slice resource data serialization failed: %v", err)
}

d.SetId(templateId)

return nil
}
105 changes: 105 additions & 0 deletions env0/data_source_code_variables_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package env0

import (
"errors"
"regexp"
"testing"

"github.com/env0/terraform-provider-env0/client"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func TestSourceCodeVariablesDataSource(t *testing.T) {
template := client.Template{
Id: "id0",
Name: "template0",
Description: "description0",
Repository: "env0/repo",
Path: "path/zero",
Revision: "branch-zero",
Type: "terraform",
TokenId: "1",
GitlabProjectId: 10,
TerraformVersion: "0.12.24",
}

payload := &client.VariablesFromRepositoryPayload{
BitbucketClientKey: template.BitbucketClientKey,
GithubInstallationId: template.GithubInstallationId,
Path: template.Path,
Revision: template.Revision,
TokenId: template.TokenId,
Repository: template.Repository,
}

var1 := client.ConfigurationVariable{
Id: "id0",
Name: "name0",
Description: "desc0",
ScopeId: "scope0",
Value: "value0",
OrganizationId: "organization0",
UserId: "user0",
Schema: &client.ConfigurationVariableSchema{Type: "string", Format: client.HCL},
Regex: "regex",
}

var2 := client.ConfigurationVariable{
Id: "id1",
Name: "name1",
Description: "desc1",
ScopeId: "scope1",
Value: "value1",
OrganizationId: "organization0",
UserId: "user1",
Schema: &client.ConfigurationVariableSchema{Type: "string", Format: client.JSON},
Regex: "regex",
}

vars := []client.ConfigurationVariable{var1, var2}

resourceType := "env0_source_code_variables"
resourceName := "test"
accessor := dataSourceAccessor(resourceType, resourceName)

testCase := resource.TestCase{
Steps: []resource.TestStep{
{
Config: dataSourceConfigCreate(resourceType, resourceName, map[string]interface{}{"template_id": template.Id}),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(accessor, "variables.0.name", var1.Name),
resource.TestCheckResourceAttr(accessor, "variables.1.name", var2.Name),
resource.TestCheckResourceAttr(accessor, "variables.0.value", var1.Value),
resource.TestCheckResourceAttr(accessor, "variables.1.value", var2.Value),
resource.TestCheckResourceAttr(accessor, "variables.0.format", string(var1.Schema.Format)),
resource.TestCheckResourceAttr(accessor, "variables.1.format", string(var2.Schema.Format)),
),
},
},
}

t.Run("Success", func(t *testing.T) {
runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) {
mock.EXPECT().Template(template.Id).AnyTimes().Return(template, nil)
mock.EXPECT().VariablesFromRepository(payload).AnyTimes().Return(vars, nil)
})
})

t.Run("API Call Error", func(t *testing.T) {
runUnitTest(t,
resource.TestCase{
Steps: []resource.TestStep{
{
Config: dataSourceConfigCreate(resourceType, resourceName, map[string]interface{}{"template_id": template.Id}),
ExpectError: regexp.MustCompile("error"),
},
},
},
func(mock *client.MockApiClientInterface) {
mock.EXPECT().Template(template.Id).AnyTimes().Return(template, nil)
mock.EXPECT().VariablesFromRepository(payload).AnyTimes().Return(nil, errors.New("error"))
},
)
})

}
1 change: 1 addition & 0 deletions env0/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func Provider(version string) plugin.ProviderFunc {
"env0_agents": dataAgents(),
"env0_user": dataUser(),
"env0_cloud_credentials": dataCloudCredentials(),
"env0_source_code_variables": dataSourceCodeVariables(),
},
ResourcesMap: map[string]*schema.Resource{
"env0_project": resourceProject(),
Expand Down
Loading

0 comments on commit f1a1a2b

Please sign in to comment.