Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/actions/test-go-tfe/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ runs:
GITHUB_REGISTRY_MODULE_IDENTIFIER: "hashicorp/terraform-random-module"
GITHUB_REGISTRY_NO_CODE_MODULE_IDENTIFIER: "hashicorp/terraform-random-no-code-module"
OAUTH_CLIENT_GITHUB_TOKEN: "${{ inputs.oauth-client-github-token }}"
SKIP_HYOK_INTEGRATION_TESTS: "${{ inputs.skip-hyok-integration-tests }}"
HYOK_ORGANIZATION_NAME: "${{ inputs.hyok-organization-name }}"
HYOK_WORKSPACE_NAME: "${{ inputs.hyok-workspace-name }}"
HYOK_POOL_ID: "${{ inputs.hyok-pool-id }}"
HYOK_PLAN_ID: "${{ inputs.hyok-plan-id }}"
HYOK_STATE_VERSION_ID: "${{ inputs.hyok-state-version-id }}"
HYOK_CUSTOMER_KEY_VERSION_ID: "${{ inputs.hyok-customer-key-version-id }}"
HYOK_ENCRYPTED_DATA_KEY_ID: "${{ inputs.hyok-encrypted-data-key-id }}"
GO111MODULE: "on"
ENABLE_TFE: ${{ inputs.enterprise }}
run: |
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Enhancements
* Exports the StackConfiguration UploadTarGzip receiver function[#1219](https://github.com/hashicorp/go-tfe/pull/1219)
* Adds support for Hold Your Own Key [#1201](https://github.com/hashicorp/go-tfe/pull/1201)

# v1.92.0

Expand Down
14 changes: 8 additions & 6 deletions agent_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,20 @@ type AgentPool struct {
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`

// Relations
Organization *Organization `jsonapi:"relation,organization"`
Workspaces []*Workspace `jsonapi:"relation,workspaces"`
AllowedWorkspaces []*Workspace `jsonapi:"relation,allowed-workspaces"`
AllowedProjects []*Project `jsonapi:"relation,allowed-projects"`
ExcludedWorkspaces []*Workspace `jsonapi:"relation,excluded-workspaces"`
Organization *Organization `jsonapi:"relation,organization"`
HYOKConfigurations []*HYOKConfiguration `jsonapi:"relation,hyok-configurations"`
Workspaces []*Workspace `jsonapi:"relation,workspaces"`
AllowedWorkspaces []*Workspace `jsonapi:"relation,allowed-workspaces"`
AllowedProjects []*Project `jsonapi:"relation,allowed-projects"`
ExcludedWorkspaces []*Workspace `jsonapi:"relation,excluded-workspaces"`
}

// A list of relations to include
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/agents#available-related-resources
type AgentPoolIncludeOpt string

const AgentPoolWorkspaces AgentPoolIncludeOpt = "workspaces"
const AgentPoolHYOKConfigurations AgentPoolIncludeOpt = "hyok-configurations"

type AgentPoolReadOptions struct {
Include []AgentPoolIncludeOpt `url:"include,omitempty"`
Expand Down Expand Up @@ -188,7 +190,7 @@ func (s *agentPools) ReadWithOptions(ctx context.Context, agentpoolID string, op
}

u := fmt.Sprintf("agent-pools/%s", url.PathEscape(agentpoolID))
req, err := s.client.NewRequest("GET", u, nil)
req, err := s.client.NewRequest("GET", u, &options)
if err != nil {
return nil, err
}
Expand Down
17 changes: 17 additions & 0 deletions agent_pool_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package tfe

import (
"context"
"os"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -343,6 +344,22 @@ func TestAgentPoolsRead(t *testing.T) {
require.NoError(t, err)
assert.NotEmpty(t, k.Workspaces[0])
})

t.Run("read hyok configurations of an agent pool", func(t *testing.T) {
skipHYOKIntegrationTests(t)

// replace the environment variable with a valid agent pool ID that has HYOK configurations
hyokPoolID := os.Getenv("HYOK_POOL_ID")
if hyokPoolID == "" {
t.Fatal("Export a valid HYOK_POOL_ID before running this test!")
}

k, err := client.AgentPools.ReadWithOptions(ctx, hyokPoolID, &AgentPoolReadOptions{
Include: []AgentPoolIncludeOpt{AgentPoolHYOKConfigurations},
})
require.NoError(t, err)
assert.NotEmpty(t, k.HYOKConfigurations)
})
}

func TestAgentPoolsReadCreatedAt(t *testing.T) {
Expand Down
147 changes: 147 additions & 0 deletions aws_oidc_configuration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package tfe

import (
"context"
"fmt"
"net/url"
)

const OIDCConfigPathFormat = "oidc-configurations/%s"

// AWSOIDCConfigurations describes all the AWS OIDC configuration related methods that the HCP Terraform API supports.
// HCP Terraform API docs:
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/hold-your-own-key/oidc-configurations/aws
type AWSOIDCConfigurations interface {
Create(ctx context.Context, organization string, options AWSOIDCConfigurationCreateOptions) (*AWSOIDCConfiguration, error)

Read(ctx context.Context, oidcID string) (*AWSOIDCConfiguration, error)

Update(ctx context.Context, oidcID string, options AWSOIDCConfigurationUpdateOptions) (*AWSOIDCConfiguration, error)

Delete(ctx context.Context, oidcID string) error
}

type awsOIDCConfigurations struct {
client *Client
}

var _ AWSOIDCConfigurations = &awsOIDCConfigurations{}

type AWSOIDCConfiguration struct {
ID string `jsonapi:"primary,aws-oidc-configurations"`
RoleARN string `jsonapi:"attr,role-arn"`

Organization *Organization `jsonapi:"relation,organization"`
}

type AWSOIDCConfigurationCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,aws-oidc-configurations"`

// Attributes
RoleARN string `jsonapi:"attr,role-arn"`
}

type AWSOIDCConfigurationUpdateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,aws-oidc-configurations"`

// Attributes
RoleARN string `jsonapi:"attr,role-arn"`
}

func (o *AWSOIDCConfigurationCreateOptions) valid() error {
if o.RoleARN == "" {
return ErrRequiredRoleARN
}

return nil
}

func (aoc *awsOIDCConfigurations) Create(ctx context.Context, organization string, options AWSOIDCConfigurationCreateOptions) (*AWSOIDCConfiguration, error) {
if !validStringID(&organization) {
return nil, ErrInvalidOrg
}

if err := options.valid(); err != nil {
return nil, err
}

req, err := aoc.client.NewRequest("POST", fmt.Sprintf("organizations/%s/oidc-configurations", organization), &options)
if err != nil {
return nil, err
}

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

return awsOIDCConfiguration, nil
}

func (aoc *awsOIDCConfigurations) Read(ctx context.Context, oidcID string) (*AWSOIDCConfiguration, error) {
req, err := aoc.client.NewRequest("GET", fmt.Sprintf(OIDCConfigPathFormat, url.PathEscape(oidcID)), nil)
if err != nil {
return nil, err
}

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

return awsOIDCConfiguration, nil
}

func (o *AWSOIDCConfigurationUpdateOptions) valid() error {
if o.RoleARN == "" {
return ErrRequiredRoleARN
}

return nil
}

func (aoc *awsOIDCConfigurations) Update(ctx context.Context, oidcID string, options AWSOIDCConfigurationUpdateOptions) (*AWSOIDCConfiguration, error) {
if !validStringID(&oidcID) {
return nil, ErrInvalidOIDC
}

if err := options.valid(); err != nil {
return nil, err
}

req, err := aoc.client.NewRequest("PATCH", fmt.Sprintf(OIDCConfigPathFormat, url.PathEscape(oidcID)), &options)
if err != nil {
return nil, err
}

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

return awsOIDCConfiguration, nil
}

func (aoc *awsOIDCConfigurations) Delete(ctx context.Context, oidcID string) error {
if !validStringID(&oidcID) {
return ErrInvalidOIDC
}

req, err := aoc.client.NewRequest("DELETE", fmt.Sprintf(OIDCConfigPathFormat, url.PathEscape(oidcID)), nil)
if err != nil {
return err
}

return req.Do(ctx, nil)
}
122 changes: 122 additions & 0 deletions aws_oidc_configuration_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package tfe

import (
"context"
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// These tests are intended for local execution only, as OIDC configurations for HYOK requires specific conditions.
// To run them locally, follow the instructions outlined in hyok_configuration_integration_test.go

func TestAWSOIDCConfigurationCreateDelete(t *testing.T) {
skipHYOKIntegrationTests(t)

client := testClient(t)
ctx := context.Background()

// replace the environment variable with a valid organization name that has AWS OIDC HYOK configurations
hyokOrganizationName := os.Getenv("HYOK_ORGANIZATION_NAME")
if hyokOrganizationName == "" {
t.Fatal("Export a valid HYOK_ORGANIZATION_NAME before running this test!")
}

orgTest, err := client.Organizations.Read(ctx, hyokOrganizationName)
if err != nil {
t.Fatal(err)
}

t.Run("with valid options", func(t *testing.T) {
opts := AWSOIDCConfigurationCreateOptions{
RoleARN: "arn:aws:iam::123456789012:role/some-role",
}

oidcConfig, err := client.AWSOIDCConfigurations.Create(ctx, orgTest.Name, opts)
require.NoError(t, err)
require.NotNil(t, oidcConfig)
assert.Equal(t, oidcConfig.RoleARN, opts.RoleARN)

// delete the created configuration
err = client.AWSOIDCConfigurations.Delete(ctx, oidcConfig.ID)
require.NoError(t, err)
})

t.Run("missing role ARN", func(t *testing.T) {
opts := AWSOIDCConfigurationCreateOptions{}

_, err := client.AWSOIDCConfigurations.Create(ctx, orgTest.Name, opts)
assert.ErrorIs(t, err, ErrRequiredRoleARN)
})
}

func TestAWSOIDCConfigurationRead(t *testing.T) {
skipHYOKIntegrationTests(t)

client := testClient(t)
ctx := context.Background()

// replace the environment variable with a valid organization name that has AWS OIDC HYOK configurations
hyokOrganizationName := os.Getenv("HYOK_ORGANIZATION_NAME")
if hyokOrganizationName == "" {
t.Fatal("Export a valid HYOK_ORGANIZATION_NAME before running this test!")
}

orgTest, err := client.Organizations.Read(ctx, hyokOrganizationName)
if err != nil {
t.Fatal(err)
}

oidcConfig, oidcConfigCleanup := createAWSOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcConfigCleanup)

t.Run("fetch existing configuration", func(t *testing.T) {
fetched, err := client.AWSOIDCConfigurations.Read(ctx, oidcConfig.ID)
require.NoError(t, err)
require.NotEmpty(t, fetched)
})

t.Run("fetching non-existing configuration", func(t *testing.T) {
_, err := client.AWSOIDCConfigurations.Read(ctx, "awsoidc-notreal")
assert.ErrorIs(t, err, ErrResourceNotFound)
})
}

func TestAWSOIDCConfigurationsUpdate(t *testing.T) {
skipHYOKIntegrationTests(t)

client := testClient(t)
ctx := context.Background()

// replace the environment variable with a valid organization name that has AWS OIDC HYOK configurations
hyokOrganizationName := os.Getenv("HYOK_ORGANIZATION_NAME")
if hyokOrganizationName == "" {
t.Fatal("Export a valid HYOK_ORGANIZATION_NAME before running this test!")
}

orgTest, err := client.Organizations.Read(ctx, hyokOrganizationName)
if err != nil {
t.Fatal(err)
}

oidcConfig, oidcConfigCleanup := createAWSOIDCConfiguration(t, client, orgTest)
t.Cleanup(oidcConfigCleanup)

t.Run("with valid options", func(t *testing.T) {
opts := AWSOIDCConfigurationUpdateOptions{
RoleARN: "arn:aws:iam::123456789012:role/some-role-2",
}
updated, err := client.AWSOIDCConfigurations.Update(ctx, oidcConfig.ID, opts)
require.NoError(t, err)
require.NotEmpty(t, updated)
assert.Equal(t, opts.RoleARN, updated.RoleARN)
})

t.Run("missing role ARN", func(t *testing.T) {
opts := AWSOIDCConfigurationUpdateOptions{}
_, err := client.AWSOIDCConfigurations.Update(ctx, oidcConfig.ID, opts)
assert.ErrorIs(t, err, ErrRequiredRoleARN)
})
}
Loading
Loading