Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a14f5ca
Added support for redirection service
mcasperson Jun 8, 2025
2bb39a4
Added some more logging
mcasperson Jun 8, 2025
bf3299e
Fixed logic
mcasperson Jun 8, 2025
f103cd5
Fixed logging
mcasperson Jun 8, 2025
f6cd7a5
redirect to the upstream host
mcasperson Jun 9, 2025
4c97525
Added more logging and fixed debugging
mcasperson Jun 9, 2025
f6e445b
Also needed to catch data sources
mcasperson Jun 9, 2025
a06fdd1
Adding retry to work around https://github.com/OctopusDeploy/terrafor…
mcasperson Jun 13, 2025
6b2c6a9
Increasing the retries
mcasperson Jun 14, 2025
40d6c4a
Fixed the logging
mcasperson Jun 14, 2025
cc84610
Fixed the logging
mcasperson Jun 14, 2025
f566e54
Increase the retry time
mcasperson Jun 14, 2025
0689322
Retry the creation of the variable if it is not found in a freshly re…
mcasperson Jun 15, 2025
372f909
Fixed error checking
mcasperson Jun 15, 2025
12e240f
fixed logging
mcasperson Jun 15, 2025
b668d05
Merge branch 'main' into mattc/spacebuilder
mcasperson Jun 23, 2025
7a2f12b
Don't do so many retries
mcasperson Jun 23, 2025
20d539d
Merge branch 'main' into mattc/spacebuilder
mcasperson Aug 6, 2025
42bad26
Merge branch 'main' into mattc/spacebuilder
mcasperson Aug 14, 2025
9510eef
Merge branch 'main' into mattc/spacebuilder
mcasperson Sep 11, 2025
33d4c3f
Respect REDIRECTION_FORCE and REDIRECTION_BYPASS
mcasperson Sep 11, 2025
a7eabcc
Respect REDIRECTION_FORCE and REDIRECTION_BYPASS
mcasperson Sep 11, 2025
8fe41fa
Fixed import
mcasperson Sep 11, 2025
313e654
Merge branch 'main' into mattc/spacebuilder
mcasperson Oct 10, 2025
72d6941
Merge branch 'main' into mattc/spacebuilder
mcasperson Oct 10, 2025
534c412
Merge branch 'main' into mattc/spacebuilder
mcasperson Oct 15, 2025
fee94da
Merge branch 'main' into mattc/spacebuilder
mcasperson Nov 3, 2025
10d1538
Merge branch 'main' into mattc/spacebuilder
mcasperson Nov 21, 2025
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
4 changes: 2 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main

Check warning on line 1 in main.go

View workflow job for this annotation

GitHub Actions / build

should have a package comment

import (
"context"
Expand Down Expand Up @@ -41,10 +41,10 @@

opts := []tf6server.ServeOpt{}

var providerName = "registry.terraform.io/OctopusDeployLabs/octopusdeploy"
var providerName = "registry.opentofu.org/octopusdeploy/octopusdeploy"
if debugMode {
opts = append(opts, tf6server.WithManagedDebug())
providerName = "octopus.com/com/octopusdeploy"
//providerName = "octopus.com/com/octopusdeploy"
}

err = tf6server.Serve(providerName, muxServer.ProviderServer, opts...)
Expand Down
139 changes: 132 additions & 7 deletions octopusdeploy/config.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
package octopusdeploy

import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"slices"
"strings"

"github.com/hashicorp/terraform-plugin-log/tflog"

"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces"
Expand All @@ -17,9 +26,115 @@ type Config struct {
SpaceID string
}

// Start of OctoAI patch

type headerRoundTripper struct {
Transport http.RoundTripper
Headers map[string]string
}

func (h *headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
for key, value := range h.Headers {
req.Header.Set(key, value)
}
return h.Transport.RoundTrip(req)
}

func getHttpClient(ctx context.Context, octopusUrl *url.URL) (*http.Client, *url.URL, error) {
if !isDirectlyAccessibleOctopusInstance(octopusUrl) {
tflog.Info(ctx, "[SPACEBUILDER] Enabled Octopus AI Assistant redirection service")
return createHttpClient(octopusUrl)
}

tflog.Info(ctx, "[SPACEBUILDER] Did not enable Octopus AI Assistant redirection service")

return nil, octopusUrl, nil
}

func getRedirectionBypass() []string {
hostnames := []string{}
hostnamesJson := os.Getenv("REDIRECTION_BYPASS")
if hostnamesJson == "" {
return []string{} // Default to empty slice if not set
}

err := json.Unmarshal([]byte(hostnamesJson), &hostnames)
if err != nil {
return []string{}
}

return hostnames
}

func getRedirectionForce() bool {
redirectionForce := os.Getenv("REDIRECTION_FORCE")
return strings.ToLower(redirectionForce) == "true"
}

// isDirectlyAccessibleOctopusInstance determines if the host should be contacted directly
func isDirectlyAccessibleOctopusInstance(octopusUrl *url.URL) bool {
serviceEnabled, found := os.LookupEnv("REDIRECTION_SERVICE_ENABLED")

if !found || serviceEnabled != "true" {
return true
}

bypassList := getRedirectionBypass()

// Allow bypassing specific domains via environment variable
if slices.Contains(bypassList, octopusUrl.Hostname()) {
return true
}

// Allow forcing all traffic through the redirection service
if getRedirectionForce() {
return false
}

return strings.HasSuffix(octopusUrl.Hostname(), ".octopus.app") ||
strings.HasSuffix(octopusUrl.Hostname(), ".testoctopus.com") ||
octopusUrl.Hostname() == "localhost" ||
octopusUrl.Hostname() == "127.0.0.1"
}

func createHttpClient(octopusUrl *url.URL) (*http.Client, *url.URL, error) {

serviceApiKey, found := os.LookupEnv("REDIRECTION_SERVICE_API_KEY")

if !found {
return nil, nil, errors.New("REDIRECTION_SERVICE_API_KEY is required")
}

redirectionHost, found := os.LookupEnv("REDIRECTION_HOST")

if !found {
return nil, nil, errors.New("REDIRECTION_HOST is required")
}

redirectionHostUrl, err := url.Parse("https://" + redirectionHost)

if err != nil {
return nil, nil, err
}

headers := map[string]string{
"X_REDIRECTION_UPSTREAM_HOST": octopusUrl.Hostname(),
"X_REDIRECTION_SERVICE_API_KEY": serviceApiKey,
}

return &http.Client{
Transport: &headerRoundTripper{
Transport: http.DefaultTransport,
Headers: headers,
},
}, redirectionHostUrl, nil
}

// End of OctoAI patch

// Client returns a new Octopus Deploy client
func (c *Config) Client() (*client.Client, diag.Diagnostics) {
octopus, err := getClientForDefaultSpace(c)
func (c *Config) Client(ctx context.Context) (*client.Client, diag.Diagnostics) {
octopus, err := getClientForDefaultSpace(ctx, c)
if err != nil {
return nil, diag.FromErr(err)
}
Expand All @@ -30,7 +145,7 @@ func (c *Config) Client() (*client.Client, diag.Diagnostics) {
return nil, diag.FromErr(err)
}

octopus, err = getClientForSpace(c, space.GetID())
octopus, err = getClientForSpace(ctx, c, space.GetID())
if err != nil {
return nil, diag.FromErr(err)
}
Expand All @@ -39,11 +154,11 @@ func (c *Config) Client() (*client.Client, diag.Diagnostics) {
return octopus, nil
}

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

func getClientForSpace(c *Config, spaceID string) (*client.Client, error) {
func getClientForSpace(ctx context.Context, c *Config, spaceID string) (*client.Client, error) {
apiURL, err := url.Parse(c.Address)
if err != nil {
return nil, err
Expand All @@ -54,7 +169,17 @@ func getClientForSpace(c *Config, spaceID string) (*client.Client, error) {
return nil, err
}

return client.NewClientWithCredentials(nil, apiURL, credential, spaceID, "TerraformProvider")
// Start of OctoAI patch
httpClient, url, err := getHttpClient(ctx, apiURL)
if err != nil {
return nil, err
}

tflog.Info(ctx, "[SPACEBUILDER] Directing requests from "+apiURL.String())
tflog.Info(ctx, "[SPACEBUILDER] Directing requests to redirector at "+url.String())

return client.NewClientWithCredentials(httpClient, url, credential, spaceID, "TerraformProvider")
// End of OctoAI patch
}

func getApiCredential(c *Config) (client.ICredential, error) {
Expand Down
2 changes: 1 addition & 1 deletion octopusdeploy/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,5 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData) (interface{}
config.SpaceID = spaceID.(string)
}

return config.Client()
return config.Client(ctx)
}
130 changes: 127 additions & 3 deletions octopusdeploy_framework/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@ package octopusdeploy_framework

import (
"context"
"encoding/json"
"errors"
"fmt"
"go/version"
"net/http"
"net/url"
"os"
"slices"
"strings"

"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/configuration"
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-log/tflog"
"go/version"
"net/url"
)

type Config struct {
Expand All @@ -25,6 +32,112 @@ type Config struct {
FeatureToggles map[string]bool
}

// Start of OctoAI patch

type headerRoundTripper struct {
Transport http.RoundTripper
Headers map[string]string
}

func (h *headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
for key, value := range h.Headers {
req.Header.Set(key, value)
}
return h.Transport.RoundTrip(req)
}

func getHttpClient(ctx context.Context, octopusUrl *url.URL) (*http.Client, *url.URL, error) {
if !isDirectlyAccessibleOctopusInstance(octopusUrl) {
tflog.Info(ctx, "[SPACEBUILDER] Enabled Octopus AI Assistant redirection service")
return createHttpClient(octopusUrl)
}

tflog.Info(ctx, "[SPACEBUILDER] Did not enable Octopus AI Assistant redirection service")

return nil, octopusUrl, nil
}

func getRedirectionBypass() []string {
hostnames := []string{}
hostnamesJson := os.Getenv("REDIRECTION_BYPASS")
if hostnamesJson == "" {
return []string{} // Default to empty slice if not set
}

err := json.Unmarshal([]byte(hostnamesJson), &hostnames)
if err != nil {
return []string{}
}

return hostnames
}

func getRedirectionForce() bool {
redirectionForce := os.Getenv("REDIRECTION_FORCE")
return strings.ToLower(redirectionForce) == "true"
}

// isDirectlyAccessibleOctopusInstance determines if the host should be contacted directly
func isDirectlyAccessibleOctopusInstance(octopusUrl *url.URL) bool {
serviceEnabled, found := os.LookupEnv("REDIRECTION_SERVICE_ENABLED")

if !found || serviceEnabled != "true" {
return true
}

bypassList := getRedirectionBypass()

// Allow bypassing specific domains via environment variable
if slices.Contains(bypassList, octopusUrl.Hostname()) {
return true
}

// Allow forcing all traffic through the redirection service
if getRedirectionForce() {
return false
}

return strings.HasSuffix(octopusUrl.Hostname(), ".octopus.app") ||
strings.HasSuffix(octopusUrl.Hostname(), ".testoctopus.com") ||
octopusUrl.Hostname() == "localhost" ||
octopusUrl.Hostname() == "127.0.0.1"
}

func createHttpClient(octopusUrl *url.URL) (*http.Client, *url.URL, error) {

serviceApiKey, found := os.LookupEnv("REDIRECTION_SERVICE_API_KEY")

if !found {
return nil, nil, errors.New("REDIRECTION_SERVICE_API_KEY is required")
}

redirectionHost, found := os.LookupEnv("REDIRECTION_HOST")

if !found {
return nil, nil, errors.New("REDIRECTION_HOST is required")
}

redirectionHostUrl, err := url.Parse("https://" + redirectionHost)

if err != nil {
return nil, nil, err
}

headers := map[string]string{
"X_REDIRECTION_UPSTREAM_HOST": octopusUrl.Hostname(),
"X_REDIRECTION_SERVICE_API_KEY": serviceApiKey,
}

return &http.Client{
Transport: &headerRoundTripper{
Transport: http.DefaultTransport,
Headers: headers,
},
}, redirectionHostUrl, nil
}

// End of OctoAI patch

func (c *Config) SetOctopus(ctx context.Context) diag.Diagnostics {
tflog.Debug(ctx, "SetOctopus")

Expand Down Expand Up @@ -122,7 +235,18 @@ func getClientForSpace(c *Config, ctx context.Context, spaceID string) (*client.
return nil, err
}

return client.NewClientWithCredentials(nil, apiURL, credential, spaceID, "TerraformProvider")
// Start of OctoAI patch
httpClient, url, err := getHttpClient(ctx, apiURL)
if err != nil {
return nil, err
}

tflog.Info(ctx, "[SPACEBUILDER] Directing requests from "+apiURL.String())
tflog.Info(ctx, "[SPACEBUILDER] Directing requests to redirector at "+url.String())

return client.NewClientWithCredentials(httpClient, url, credential, spaceID, "TerraformProvider")

// End of OctoAI patch
}

func getApiCredential(c *Config, ctx context.Context) (client.ICredential, error) {
Expand Down
Loading
Loading