diff --git a/main.go b/main.go index 1e93c165..6aaec7f0 100644 --- a/main.go +++ b/main.go @@ -41,10 +41,10 @@ func main() { 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...) diff --git a/octopusdeploy/config.go b/octopusdeploy/config.go index 037171ed..2475089a 100644 --- a/octopusdeploy/config.go +++ b/octopusdeploy/config.go @@ -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" @@ -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) } @@ -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) } @@ -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 @@ -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) { diff --git a/octopusdeploy/provider.go b/octopusdeploy/provider.go index b09c1e89..24eca58b 100644 --- a/octopusdeploy/provider.go +++ b/octopusdeploy/provider.go @@ -100,5 +100,5 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData) (interface{} config.SpaceID = spaceID.(string) } - return config.Client() + return config.Client(ctx) } diff --git a/octopusdeploy_framework/config.go b/octopusdeploy_framework/config.go index 7d0b3a55..436a5d0b 100644 --- a/octopusdeploy_framework/config.go +++ b/octopusdeploy_framework/config.go @@ -2,7 +2,16 @@ 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" @@ -10,8 +19,6 @@ import ( "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 { @@ -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") @@ -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) { diff --git a/octopusdeploy_framework/resource_variable.go b/octopusdeploy_framework/resource_variable.go index 0e1d195a..54620eb6 100644 --- a/octopusdeploy_framework/resource_variable.go +++ b/octopusdeploy_framework/resource_variable.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "time" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/variables" "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal" @@ -90,15 +91,29 @@ func (r *variableTypeResource) Create(ctx context.Context, req resource.CreateRe tflog.Info(ctx, fmt.Sprintf("creating variable: %#v", newVariable)) - variableSet, err := variables.AddSingle(r.Config.Client, data.SpaceID.ValueString(), variableOwnerId.ValueString(), newVariable) - if err != nil { - resp.Diagnostics.AddError("create variable failed", err.Error()) - return + // Start of OctoAI patch + // Retry logic to address the issue documented at https://github.com/OctopusDeploy/terraform-provider-octopusdeploy/issues/29 + var validateError error + for i := 0; i < 10; i++ { + variableSet, err := variables.AddSingle(r.Config.Client, data.SpaceID.ValueString(), variableOwnerId.ValueString(), newVariable) + if err != nil { + resp.Diagnostics.AddError("create variable failed", err.Error()) + return + } + + validateError = validateVariable(&variableSet, newVariable, variableOwnerId.ValueString()) + + if validateError == nil { + break + } + + tflog.Info(ctx, "retrying to create variable "+newVariable.Name+": "+fmt.Sprint(i)) + time.Sleep(time.Second) } + // End of OctoAI patch - err = validateVariable(&variableSet, newVariable, variableOwnerId.ValueString()) - if err != nil { - resp.Diagnostics.AddError("create variable failed", err.Error()) + if validateError != nil { + resp.Diagnostics.AddError("create variable failed", validateError.Error()) return }