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

Feature: Add support for "Service Accounts" #545

Merged
Merged
Show file tree
Hide file tree
Changes from 2 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: 37 additions & 7 deletions internal/definition/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@
return &schema.Provider{
Schema: map[string]*schema.Schema{
"auth_token": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("SFX_AUTH_TOKEN", ""),
Description: "Splunk Observability Cloud auth token",
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"email", "password", "organization_id"},
DefaultFunc: schema.EnvDefaultFunc("SFX_AUTH_TOKEN", ""),
Description: "Splunk Observability Cloud auth token",
},
"api_url": {
Type: schema.TypeString,
Expand Down Expand Up @@ -70,6 +71,24 @@
Default: 30,
Description: "Maximum retry wait for a single HTTP call in seconds. Defaults to 30",
},
"email": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"auth_token"},
Description: "Used to create a session token instead of an API token, it requires the account to be configured to login with Email and Password",
},
"password": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"auth_token"},
Description: "Used to create a session token instead of an API token, it requires the account to be configured to login with Email and Password",
},
"organization_id": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"auth_token"},
Description: "Required if the user is configured to be part of multiple organizations",
},
},
ResourcesMap: map[string]*schema.Resource{
team.ResourceName: team.NewResource(),
Expand All @@ -83,9 +102,14 @@
}

func configureProvider(ctx context.Context, data *schema.ResourceData) (any, diag.Diagnostics) {
var meta pmeta.Meta
meta := &pmeta.Meta{
Email: data.Get("email").(string),
Password: data.Get("password").(string),
OrganizationID: data.Get("organization_id").(string),
}

for _, lookup := range pmeta.NewDefaultProviderLookups() {
if err := lookup.Do(ctx, &meta); err != nil {
if err := lookup.Do(ctx, meta); err != nil {
tflog.Debug(
ctx,
"Issue trying to load external provider configuration, skipping",
Expand Down Expand Up @@ -116,6 +140,11 @@
waitmax = time.Duration(int64((data.Get("retry_wait_max_seconds").(int)))) * time.Second
)

token, err := meta.LoadSessionToken(ctx)
if err != nil {
return nil, tfext.AsErrorDiagnostics(err)
}

Check warning on line 146 in internal/definition/provider/provider.go

View check run for this annotation

Codecov / codecov/patch

internal/definition/provider/provider.go#L145-L146

Added lines #L145 - L146 were not covered by tests

rc := retryablehttp.NewClient()
rc.RetryMax = attempts
rc.RetryWaitMin = waitmin
Expand All @@ -129,7 +158,8 @@
MaxIdleConnsPerHost: 100,
})

meta.Client, err = signalfx.NewClient(meta.AuthToken,
meta.Client, err = signalfx.NewClient(
token,
signalfx.APIUrl(meta.APIURL),
signalfx.HTTPClient(rc.StandardClient()),
signalfx.UserAgent(fmt.Sprintf("Terraform terraform-provider-signalfx/%s", version.ProviderVersion)),
Expand Down
2 changes: 1 addition & 1 deletion internal/definition/provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func TestProviderConfiguration(t *testing.T) {
name: "no details provided",
details: make(map[string]any),
expect: diag.Diagnostics{
{Severity: diag.Error, Summary: "auth token not set"},
{Severity: diag.Error, Summary: "missing auth token or email and password"},
},
},
{
Expand Down
47 changes: 39 additions & 8 deletions internal/providermeta/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/signalfx/signalfx-go"
"github.com/signalfx/signalfx-go/sessiontoken"
"go.uber.org/multierr"

tfext "github.com/splunk-terraform/terraform-provider-signalfx/internal/tfextension"
Expand All @@ -27,10 +28,13 @@
// It is abstracted out from the provider definition to make it easier
// to test CRUD operations within unit tests.
type Meta struct {
AuthToken string `json:"auth_token"`
APIURL string `json:"api_url"`
CustomAppURL string `json:"custom_app_url"`
Client *signalfx.Client `json:"-"`
AuthToken string `json:"auth_token"`
APIURL string `json:"api_url"`
CustomAppURL string `json:"custom_app_url"`
Client *signalfx.Client `json:"-"`
Email string `json:"email"`
Password string `json:"password"`
OrganizationID string `json:"org_id"`
}

// LoadClient returns the configured [signalfx.Client] ready to use.
Expand Down Expand Up @@ -68,11 +72,38 @@
return u.String()
}

func (s *Meta) Validate() (errs error) {
if s.AuthToken == "" {
errs = multierr.Append(errs, errors.New("auth token not set"))
// LoadSessionToken will use the provider username and password
// so that it can be used as the token through the interaction.
func (m *Meta) LoadSessionToken(ctx context.Context) (string, error) {
if m.AuthToken != "" {
return m.AuthToken, nil
}
if s.APIURL == "" {

client, err := signalfx.NewClient("", signalfx.APIUrl(m.APIURL))
if err != nil {
return "", err
}

Check warning on line 85 in internal/providermeta/meta.go

View check run for this annotation

Codecov / codecov/patch

internal/providermeta/meta.go#L84-L85

Added lines #L84 - L85 were not covered by tests

resp, err := client.CreateSessionToken(ctx, &sessiontoken.CreateTokenRequest{
Email: m.Email,
Password: m.Password,
OrganizationId: m.OrganizationID,
})
if err != nil {
return "", err
}

// TODO: determine if any additional fields would be useful for debugging.
tflog.Info(ctx, "Created new session token")

return resp.AccessToken, nil
}

func (m *Meta) Validate() (errs error) {
if m.AuthToken == "" && (m.Email == "" && m.Password == "") {
MovieStoreGuy marked this conversation as resolved.
Show resolved Hide resolved
errs = multierr.Append(errs, errors.New("missing auth token or email and password"))
}
if m.APIURL == "" {
errs = multierr.Append(errs, errors.New("api url is not set"))
}
return errs
Expand Down
86 changes: 85 additions & 1 deletion internal/providermeta/meta_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@ package pmeta

import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/signalfx/signalfx-go/sessiontoken"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -102,7 +108,7 @@ func TestMetaValidation(t *testing.T) {
{
name: "meta not set",
meta: Meta{},
errVal: "auth token not set; api url is not set",
errVal: "missing auth token or email and password; api url is not set",
},
{
name: "state valid",
Expand All @@ -124,3 +130,81 @@ func TestMetaValidation(t *testing.T) {
})
}
}

func TestMetaToken(t *testing.T) {
t.Parallel()

for _, tc := range []struct {
name string
token string
handler http.HandlerFunc
email string
password string
expect string
errVal string
}{
{
name: "missing values",
token: "",
handler: func(w http.ResponseWriter, r *http.Request) {
_, _ = io.Copy(io.Discard, r.Body)
_ = r.Body.Close()

http.Error(w, "failed auth", http.StatusBadRequest)
},
email: "",
password: "",
expect: "",
errVal: "route \"/v2/session\" had issues with status code 400",
},
{
name: "token already provided",
token: "aaccbbb",
handler: func(w http.ResponseWriter, r *http.Request) {
_, _ = io.Copy(io.Discard, r.Body)
_ = r.Body.Close()

http.Error(w, "should not be called", http.StatusBadRequest)
},
email: "",
password: "",
expect: "aaccbbb",
errVal: "",
},
{
name: "username password provided",
token: "",
handler: func(w http.ResponseWriter, r *http.Request) {
_, _ = io.Copy(io.Discard, r.Body)
_ = r.Body.Close()

_ = json.NewEncoder(w).Encode(&sessiontoken.Token{AccessToken: "secret"})
},
email: "user@example",
password: "notsosecret",
expect: "secret",
errVal: "",
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
s := httptest.NewServer(tc.handler)
t.Cleanup(s.Close)

m := &Meta{
APIURL: s.URL,
AuthToken: tc.token,
Email: tc.email,
Password: tc.password,
}

if token, err := m.LoadSessionToken(context.Background()); tc.errVal != "" {
assert.Equal(t, tc.expect, token, "Must match the expected value")
assert.EqualError(t, err, tc.errVal, "Must match the expected value")
} else {
assert.Equal(t, tc.expect, token, "Must match the expected value")
assert.NoError(t, err, "Must not error")
}
})
}
}
2 changes: 1 addition & 1 deletion internal/tftest/meta_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func TestNewAcceptanceConfigure(t *testing.T) {
name: "no values set",
envs: map[string]string{},
issues: diag.Diagnostics{
{Severity: diag.Error, Summary: "auth token not set"},
{Severity: diag.Error, Summary: "missing auth token or email and password"},
{Severity: diag.Error, Summary: "api url is not set"},
},
},
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package main

import (
"github.com/hashicorp/terraform-plugin-sdk/v2/plugin"

"github.com/splunk-terraform/terraform-provider-signalfx/signalfx"
)

Expand Down
45 changes: 36 additions & 9 deletions signalfx/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package signalfx

import (
"context"
"encoding/json"
"fmt"
"log"
Expand Down Expand Up @@ -37,10 +38,11 @@ func Provider() *schema.Provider {
sfxProvider = &schema.Provider{
Schema: map[string]*schema.Schema{
"auth_token": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("SFX_AUTH_TOKEN", ""),
Description: "Splunk Observability Cloud auth token",
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"email", "password", "organization_id"},
DefaultFunc: schema.EnvDefaultFunc("SFX_AUTH_TOKEN", ""),
Description: "Splunk Observability Cloud auth token",
},
"api_url": {
Type: schema.TypeString,
Expand Down Expand Up @@ -78,6 +80,24 @@ func Provider() *schema.Provider {
Default: 30,
Description: "Maximum retry wait for a single HTTP call in seconds. Defaults to 30",
},
"email": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"auth_token"},
Description: "Used to create a session token instead of an API token, it requires the account to be configured to login with Email and Password",
},
"password": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"auth_token"},
Description: "Used to create a session token instead of an API token, it requires the account to be configured to login with Email and Password",
},
"organization_id": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"auth_token"},
Description: "Required if the user is configured to be part of multiple organizations",
},
},
DataSourcesMap: map[string]*schema.Resource{
"signalfx_dimension_values": dataSourceDimensionValues(),
Expand Down Expand Up @@ -159,16 +179,17 @@ func signalfxConfigure(data *schema.ResourceData) (interface{}, error) {
config.AuthToken = token.(string)
}

if config.AuthToken == "" {
return &config, fmt.Errorf("auth_token: required field is not set")
}
if url, ok := data.GetOk("api_url"); ok {
config.APIURL = url.(string)
}
if customAppURL, ok := data.GetOk("custom_app_url"); ok {
config.CustomAppURL = customAppURL.(string)
}

if err = config.Validate(); err != nil {
return nil, err
}

netTransport := logging.NewTransport("SignalFx", &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Expand Down Expand Up @@ -199,10 +220,16 @@ func signalfxConfigure(data *schema.ResourceData) (interface{}, error) {
retryClient.HTTPClient.Transport = netTransport
standardClient := retryClient.StandardClient()

client, err := sfx.NewClient(config.AuthToken,
token, err := config.LoadSessionToken(context.Background())
if err != nil {
return nil, err
}

client, err := sfx.NewClient(
token,
sfx.APIUrl(config.APIURL),
sfx.HTTPClient(standardClient),
sfx.UserAgent(fmt.Sprintf(providerUserAgent)),
sfx.UserAgent(providerUserAgent),
)
if err != nil {
return &config, err
Expand Down
2 changes: 1 addition & 1 deletion signalfx/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func TestProviderConfigureFromNothing(t *testing.T) {
rp := Provider()
diag := rp.Configure(context.Background(), terraform.NewResourceConfigRaw(raw))
assert.NotNil(t, diag)
assert.Contains(t, diag[0].Summary, "auth_token: required field is not set")
assert.Contains(t, diag[0].Summary, "missing auth token or email and password")
}

func TestProviderConfigureFromTerraform(t *testing.T) {
Expand Down
Loading
Loading