Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for OIDC bearer tokens to auth TFP against Octopus API #810

Merged
merged 2 commits into from
Nov 11, 2024
Merged
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
44 changes: 41 additions & 3 deletions docs/guides/2-provider-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ subcategory: "Guides"

## Example usage

### API Key

`main.tf`

```hcl
Expand All @@ -25,13 +27,49 @@ provider "octopusdeploy" {
}
```

### Access Token (via Environment Variable)
OIDC Access Tokens are short-lived and typically generated per-run of an automated pipeline, such as GitHub Actions.
If you use the Access Token approach, we recommend sourcing the token from environment variable.

The environment variable fallback values that the Terraform Provider search for correspond to the values that pipeline steps like our [GitHub Login action](https://github.com/OctopusDeploy/login?tab=readme-ov-file#outputs) set in the pipeline context, so the provider will automatically pick up the value from environment variable.
Comment on lines +31 to +34
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


`main.tf`

```hcl
terraform {
required_providers {
octopusdeploy = {
source = OctopusDeployLabs/octopusdeploy
}
}
}

provider "octopusdeploy" {
space_id = "..."
}
```

## Schema

### Required
* `address` (String) The Octopus Deploy server URL. This can also be set using the `OCTOPUS_URL` environment variable.
* `api_key` (String) The Octopus Deploy server API key. This can also be set using the `OCTOPUS_APIKEY` environment variable.
* `address` (String) The Octopus Deploy server URL.

and one of either
* `api_key` (String) The Octopus Deploy server API key.

OR
* `access_token` (String) The OIDC Access Token from an OIDC exchange.

### Optional
* `space_id` (String) The ID of the space to create the resources in.

**If `space_id` is not specified the default space will be used.**
**If `space_id` is not specified the default space will be used.**

### Environment Variable fallback
The following priority order will be used to calculate the final value for these configuration items:

| Configuration Item | Priority Order |
|--------------------|--------------------------------------------------------------------------------------------------|
| `address` | 1. Provider Configuration Block <br /> 2. env: `OCTOPUS_URL` |
| `api_key` | 1. Provider Configuration Block <br /> 2. env: `OCTOPUS_APIKEY` <br /> 3. env: `OCTOPUS_API_KEY` |
| `access_token` | 1. Provider Configuration Block <br /> 2. env: `OCTOPUS_ACCESS_TOKEN` |
8 changes: 8 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ This provider is used to configure resources in Octopus Deploy. The provider mus

## Configuration

### Authentication Methods
The provider supports authenticating to an Octopus Server instance via either:
* API Key
* OIDC Access Token

These are mutually exclusive options - use either, not both. For backward compatibility, API Key will always be preferred over OIDC, when an API Key is present.

### Default Space

Octopus Deploy supports the concept of a Default Space. This is the first space that is automatically created on server setup. If you do not specify a Space when configuring the Octopus Deploy Terraform provider it will use the Default Space.
Expand Down Expand Up @@ -81,6 +88,7 @@ resource "octopusdeploy_environment" "Env3" {

### Optional

- `access_token` (String) The OIDC Access Token to use with the Octopus REST API
- `address` (String) The endpoint of the Octopus REST API
- `api_key` (String) The API key to use with the Octopus REST API
- `space_id` (String) The space ID to target
57 changes: 47 additions & 10 deletions octopusdeploy/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package octopusdeploy

import (
"fmt"
"net/url"

"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
Expand All @@ -10,19 +11,15 @@ import (

// Config holds Address and the APIKey of the Octopus Deploy server
type Config struct {
Address string
APIKey string
SpaceID string
Address string
APIKey string
AccessToken string
SpaceID string
}

// Client returns a new Octopus Deploy client
func (c *Config) Client() (*client.Client, diag.Diagnostics) {
apiURL, err := url.Parse(c.Address)
if err != nil {
return nil, diag.FromErr(err)
}

octopus, err := client.NewClient(nil, apiURL, c.APIKey, "")
octopus, err := getClientForDefaultSpace(c)
if err != nil {
return nil, diag.FromErr(err)
}
Expand All @@ -33,11 +30,51 @@ func (c *Config) Client() (*client.Client, diag.Diagnostics) {
return nil, diag.FromErr(err)
}

octopus, err = client.NewClient(nil, apiURL, c.APIKey, space.GetID())
octopus, err = getClientForSpace(c, space.GetID())
if err != nil {
return nil, diag.FromErr(err)
}
}

return octopus, nil
}

func getClientForDefaultSpace(c *Config) (*client.Client, error) {
return getClientForSpace(c, "")
}

func getClientForSpace(c *Config, spaceID string) (*client.Client, error) {
apiURL, err := url.Parse(c.Address)
if err != nil {
return nil, err
}

credential, err := getApiCredential(c)
if err != nil {
return nil, err
}

return client.NewClientWithCredentials(nil, apiURL, credential, spaceID, "TerraformProvider")
}

func getApiCredential(c *Config) (client.ICredential, error) {
if c.APIKey != "" {
credential, err := client.NewApiKey(c.APIKey)
if err != nil {
return nil, err
}

return credential, nil
}

if c.AccessToken != "" {
credential, err := client.NewAccessToken(c.AccessToken)
if err != nil {
return nil, err
}

return credential, nil
}

return nil, fmt.Errorf("either an APIKey or an AccessToken is required to connect to the Octopus Server instance")
}
13 changes: 10 additions & 3 deletions octopusdeploy/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,17 @@ func Provider() *schema.Provider {
Type: schema.TypeString,
},
"api_key": {
DefaultFunc: schema.EnvDefaultFunc("OCTOPUS_APIKEY", nil),
DefaultFunc: schema.MultiEnvDefaultFunc([]string{"OCTOPUS_APIKEY", "OCTOPUS_API_KEY"}, nil),
Description: "The API key to use with the Octopus REST API",
Optional: true,
Type: schema.TypeString,
},
"access_token": {
DefaultFunc: schema.EnvDefaultFunc("OCTOPUS_ACCESS_TOKEN", nil),
Description: "The OIDC Access Token to use with the Octopus REST API",
Optional: true,
Type: schema.TypeString,
},
"space_id": {
Description: "The space ID to target",
Optional: true,
Expand All @@ -94,8 +100,9 @@ func Provider() *schema.Provider {

func providerConfigure(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) {
config := Config{
Address: d.Get("address").(string),
APIKey: d.Get("api_key").(string),
AccessToken: d.Get("access_token").(string),
Address: d.Get("address").(string),
APIKey: d.Get("api_key").(string),
}
if spaceID, ok := d.GetOk("space_id"); ok {
config.SpaceID = spaceID.(string)
Expand Down
65 changes: 55 additions & 10 deletions octopusdeploy_framework/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,17 @@ import (
)

type Config struct {
Address string
ApiKey string
SpaceID string
Client *client.Client
Address string
ApiKey string
AccessToken string
SpaceID string
Client *client.Client
}

func (c *Config) GetClient(ctx context.Context) error {
tflog.Debug(ctx, "GetClient")
apiURL, err := url.Parse(c.Address)
if err != nil {
return err
}

octopus, err := client.NewClient(nil, apiURL, c.ApiKey, "")
octopus, err := getClientForDefaultSpace(c, ctx)
if err != nil {
return err
}
Expand All @@ -36,7 +33,7 @@ func (c *Config) GetClient(ctx context.Context) error {
return err
}

octopus, err = client.NewClient(nil, apiURL, c.ApiKey, space.GetID())
octopus, err = getClientForSpace(c, ctx, space.GetID())
if err != nil {
return err
}
Expand All @@ -49,6 +46,54 @@ func (c *Config) GetClient(ctx context.Context) error {
return nil
}

func getClientForDefaultSpace(c *Config, ctx context.Context) (*client.Client, error) {
return getClientForSpace(c, ctx, "")
}

func getClientForSpace(c *Config, ctx context.Context, spaceID string) (*client.Client, error) {
apiURL, err := url.Parse(c.Address)
if err != nil {
return nil, err
}

credential, err := getApiCredential(c, ctx)
if err != nil {
return nil, err
}

return client.NewClientWithCredentials(nil, apiURL, credential, spaceID, "TerraformProvider")
}

func getApiCredential(c *Config, ctx context.Context) (client.ICredential, error) {
tflog.Debug(ctx, "GetClient: Trying the following auth methods in order of priority - APIKey, AccessToken")

if c.ApiKey != "" {
tflog.Debug(ctx, "GetClient: Attempting to authenticate with API Key")
credential, err := client.NewApiKey(c.ApiKey)
if err != nil {
return nil, err
}

return credential, nil
} else {
tflog.Debug(ctx, "GetClient: No API Key found")
}

if c.AccessToken != "" {
tflog.Debug(ctx, "GetClient: Attempting to authenticate with Access Token")
credential, err := client.NewAccessToken(c.AccessToken)
if err != nil {
return nil, err
}

return credential, nil
} else {
tflog.Debug(ctx, "GetClient: No Access Token found")
}

return nil, fmt.Errorf("either an APIKey or an AccessToken is required to connect to the Octopus Server instance")
}

func DataSourceConfiguration(req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) *Config {
if req.ProviderData == nil {
return nil
Expand Down
17 changes: 14 additions & 3 deletions octopusdeploy_framework/framework_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import (
)

type octopusDeployFrameworkProvider struct {
Address types.String `tfsdk:"address"`
ApiKey types.String `tfsdk:"api_key"`
SpaceID types.String `tfsdk:"space_id"`
Address types.String `tfsdk:"address"`
ApiKey types.String `tfsdk:"api_key"`
AccessToken types.String `tfsdk:"access_token"`
SpaceID types.String `tfsdk:"space_id"`
}

var _ provider.Provider = (*octopusDeployFrameworkProvider)(nil)
Expand Down Expand Up @@ -45,6 +46,12 @@ func (p *octopusDeployFrameworkProvider) Configure(ctx context.Context, req prov
if config.ApiKey == "" {
config.ApiKey = os.Getenv("OCTOPUS_APIKEY")
}
if config.ApiKey == "" {
config.ApiKey = os.Getenv("OCTOPUS_API_KEY")
}
if config.AccessToken == "" {
config.AccessToken = os.Getenv("OCTOPUS_ACCESS_TOKEN")
}
config.Address = providerData.Address.ValueString()
if config.Address == "" {
config.Address = os.Getenv("OCTOPUS_URL")
Expand Down Expand Up @@ -118,6 +125,10 @@ func (p *octopusDeployFrameworkProvider) Schema(_ context.Context, req provider.
Optional: true,
Description: "The API key to use with the Octopus REST API",
},
"access_token": schema.StringAttribute{
Optional: true,
Description: "The OIDC Access Token to use with the Octopus REST API",
},
"space_id": schema.StringAttribute{
Optional: true,
Description: "The space ID to target",
Expand Down
44 changes: 41 additions & 3 deletions templates/guides/2-provider-configuration.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ subcategory: "Guides"

## Example usage

### API Key

`main.tf`

```hcl
Expand All @@ -25,13 +27,49 @@ provider "octopusdeploy" {
}
```

### Access Token (via Environment Variable)
OIDC Access Tokens are short-lived and typically generated per-run of an automated pipeline, such as GitHub Actions.
If you use the Access Token approach, we recommend sourcing the token from environment variable.

The environment variable fallback values that the Terraform Provider search for correspond to the values that pipeline steps like our [GitHub Login action](https://github.com/OctopusDeploy/login?tab=readme-ov-file#outputs) set in the pipeline context, so the provider will automatically pick up the value from environment variable.

`main.tf`

```hcl
terraform {
required_providers {
octopusdeploy = {
source = OctopusDeployLabs/octopusdeploy
}
}
}

provider "octopusdeploy" {
space_id = "..."
}
```

## Schema

### Required
* `address` (String) The Octopus Deploy server URL. This can also be set using the `OCTOPUS_URL` environment variable.
* `api_key` (String) The Octopus Deploy server API key. This can also be set using the `OCTOPUS_APIKEY` environment variable.
* `address` (String) The Octopus Deploy server URL.

and one of either
* `api_key` (String) The Octopus Deploy server API key.

OR
* `access_token` (String) The OIDC Access Token from an OIDC exchange.

### Optional
* `space_id` (String) The ID of the space to create the resources in.

**If `space_id` is not specified the default space will be used.**
**If `space_id` is not specified the default space will be used.**

### Environment Variable fallback
The following priority order will be used to calculate the final value for these configuration items:

| Configuration Item | Priority Order |
|--------------------|--------------------------------------------------------------------------------------------------|
| `address` | 1. Provider Configuration Block <br /> 2. env: `OCTOPUS_URL` |
| `api_key` | 1. Provider Configuration Block <br /> 2. env: `OCTOPUS_APIKEY` <br /> 3. env: `OCTOPUS_API_KEY` |
| `access_token` | 1. Provider Configuration Block <br /> 2. env: `OCTOPUS_ACCESS_TOKEN` |
Loading
Loading