From 4b154ce4cebe2950904da81e9b4fa7def39ce686 Mon Sep 17 00:00:00 2001 From: MinUk Song Date: Thu, 11 Jan 2024 16:24:30 +0900 Subject: [PATCH] feat: Add provider auth chain --- examples/provider/provider.tf | 30 +- internal/provider/provider.go | 280 +++++------------- internal/provider/type.go | 210 +++++++++++++ .../applications/whoami_data_source_test.go | 93 +++++- internal/testacc/provider.go | 10 +- main.go | 6 +- pkg/bluechip_authenticator/auth.go | 69 ++++- pkg/bluechip_authenticator/aws.go | 2 +- pkg/framework/fwdiag/diag.go | 21 ++ pkg/framework/fwflex/expander.go | 15 + 10 files changed, 492 insertions(+), 244 deletions(-) create mode 100644 internal/provider/type.go create mode 100644 pkg/framework/fwdiag/diag.go diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf index 59cddbe..d3176b3 100644 --- a/examples/provider/provider.tf +++ b/examples/provider/provider.tf @@ -1,32 +1,42 @@ provider "bluechip" { address = "app.bluechip.com" - basic_auth { - username = "myuser" - password = "mypassword" + auth_flow { + basic { + username = "myuser" + password = "mypassword" + } } } provider "bluechip" { alias = "token" address = "app.bluechip.com" - token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + auth_flow { + token { + token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + } + } } provider "bluechip" { alias = "aws" address = "app.bluechip.com" - aws_auth { - cluster_name = "bluechip-prod" - region = "us-east-1" + auth_flow { + aws { + cluster_name = "bluechip-prod" + region = "us-east-1" + } } } provider "bluechip" { alias = "oidc" address = "app.bluechip.com" - oidc_auth { - validator_name = "kubernetes-centre" - token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + auth_flow { + oidc { + validator_name = "kubernetes-centre" + token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + } } } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 5d8b0d6..0783b7f 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -4,9 +4,9 @@ import ( "context" "fmt" "net/http" + "os" "time" - "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" @@ -14,108 +14,34 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/pubg/terraform-provider-bluechip/pkg/bluechip_authenticator" "github.com/pubg/terraform-provider-bluechip/pkg/bluechip_client" + "github.com/pubg/terraform-provider-bluechip/pkg/framework/fwdiag" "github.com/pubg/terraform-provider-bluechip/pkg/framework/fwlog" - "github.com/pubg/terraform-provider-bluechip/pkg/framework/fwtype" ) -func Provider() *schema.Provider { +var providerAuthTyp = &ProviderAuthType{} + +func Provider(version string, commit string) func() *schema.Provider { p := &schema.Provider{ Schema: map[string]*schema.Schema{ "address": { Type: schema.TypeString, Required: true, - DefaultFunc: schema.EnvDefaultFunc("BLUECHIP_ADDRESS", ""), + DefaultFunc: schema.EnvDefaultFunc("BLUECHIP_ADDR", ""), ValidateFunc: validation.IsURLWithHTTPorHTTPS, }, - "token": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("BLUECHIP_TOKEN", ""), - }, - "basic_auth": { - Type: schema.TypeList, - MinItems: 0, - MaxItems: 1, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "username": { - Type: schema.TypeString, - Required: true, - DefaultFunc: schema.EnvDefaultFunc("BLUECHIP_USERNAME", ""), - }, - "password": { - Type: schema.TypeString, - Required: true, - DefaultFunc: schema.EnvDefaultFunc("BLUECHIP_PASSWORD", ""), - }, - }, - }, - }, - "aws_auth": { - Type: schema.TypeList, - MinItems: 0, - MaxItems: 1, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "cluster_name": { - Type: schema.TypeString, - Required: true, - DefaultFunc: schema.EnvDefaultFunc("BLUECHIP_CLUSTER_NAME", ""), - }, - "access_key": { - Type: schema.TypeString, - Optional: true, - }, - "secret_access_key": { - Type: schema.TypeString, - Optional: true, - }, - "session_token": { - Type: schema.TypeString, - Optional: true, - }, - "region": { - Type: schema.TypeString, - Optional: true, - }, - "profile": { - Type: schema.TypeString, - Optional: true, - }, - }, - }, - }, - "oidc_auth": { - Type: schema.TypeList, - MinItems: 0, - MaxItems: 1, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "validator_name": { - Type: schema.TypeString, - Required: true, - }, - "token": { - Type: schema.TypeString, - Required: true, - DefaultFunc: schema.EnvDefaultFunc("BLUECHIP_OIDC_TOKEN", ""), - }, - }, - }, - }, + "auth_flow": providerAuthTyp.Schema(), }, ResourcesMap: resourceMap, DataSourcesMap: dataSourceMap, } p.ConfigureContextFunc = func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { - return providerConfigure(ctx, d, p.TerraformVersion) + return providerConfigure(ctx, d, fmt.Sprintf("%s+%s", version, commit)) } - return p + return func() *schema.Provider { + return p + } } type ProviderModel struct { @@ -124,7 +50,10 @@ type ProviderModel struct { } func providerConfigure(ctx context.Context, d *schema.ResourceData, providerVersion string) (interface{}, diag.Diagnostics) { - config := loadProviderConfiguration(d) + config, diags := expandProviderConfig(ctx, d) + if diags.HasError() { + return nil, diags + } tflog.Debug(ctx, "Read Provider Config", fwlog.Field("config", config)) // Validate the auth configuration values @@ -151,140 +80,89 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData, providerVers } type ProviderConfig struct { - Address string - Token *string - BasicAuth *struct { - Username string - Password string - } - AwsAuth *struct { - ClusterName string - AccessKey *string - SecretAccessKey *string - SessionToken *string - Region *string - Profile *string - } - OidcAuth *struct { - ValidatorName string - Token string - } + Address string + Auths ProviderAuths } -func loadProviderConfiguration(d *schema.ResourceData) ProviderConfig { +func expandProviderConfig(ctx context.Context, d *schema.ResourceData) (*ProviderConfig, diag.Diagnostics) { var config ProviderConfig config.Address = d.Get("address").(string) - if v, ok := d.GetOk("token"); ok { - config.Token = v.(*string) - } - - // Copilot wrote this code - if v, ok := d.GetOk("basic_auth"); ok { - config.BasicAuth = &struct { - Username string - Password string - }{ - Username: v.([]interface{})[0].(map[string]interface{})["username"].(string), - Password: v.([]interface{})[0].(map[string]interface{})["password"].(string), - } - } - - if v, ok := d.GetOk("aws_auth"); ok { - attr := v.([]interface{})[0].(map[string]interface{}) - config.AwsAuth = &struct { - ClusterName string - AccessKey *string - SecretAccessKey *string - SessionToken *string - Region *string - Profile *string - }{ - ClusterName: attr["cluster_name"].(string), - } - if attr["access_key"] != nil { - config.AwsAuth.AccessKey = fwtype.String(attr["access_key"].(string)) - } - if attr["secret_access_key"] != nil { - config.AwsAuth.SecretAccessKey = fwtype.String(attr["secret_access_key"].(string)) - } - if attr["session_token"] != nil { - config.AwsAuth.SessionToken = fwtype.String(attr["session_token"].(string)) - } - if attr["region"] != nil { - config.AwsAuth.Region = fwtype.String(attr["region"].(string)) - } - if attr["profile"] != nil { - config.AwsAuth.Profile = fwtype.String(attr["profile"].(string)) - } - } - - if v, ok := d.GetOk("oidc_auth"); ok { - config.OidcAuth = &struct { - ValidatorName string - Token string - }{ - ValidatorName: v.([]interface{})[0].(map[string]interface{})["validator_name"].(string), - Token: v.([]interface{})[0].(map[string]interface{})["token"].(string), - } + if diags := providerAuthTyp.Expand(ctx, d, &config.Auths); diags.HasError() { + return nil, diags } - return config + return &config, nil } -func initializeBluechipToken(ctx context.Context, authClient *bluechip_authenticator.Client, config ProviderConfig) (string, diag.Diagnostics) { +func initializeBluechipToken(ctx context.Context, authClient *bluechip_authenticator.Client, config *ProviderConfig) (string, diag.Diagnostics) { var diags diag.Diagnostics - if config.Token != nil { - return *config.Token, nil - } - if config.BasicAuth != nil { - token, err := authClient.LoginWithBasic(context.Background(), config.BasicAuth.Username, config.BasicAuth.Password) - if err == nil { + + for index, authConfig := range config.Auths.Items { + if tokenAuth, ok := authConfig.(*TokenAuth); ok { + if tokenAuth.Token != "" { + return tokenAuth.Token, nil + } + } else if basicAuth, ok := authConfig.(*BasicAuth); ok { + token, err := authClient.LoginWithBasic(context.Background(), basicAuth.Username, basicAuth.Password) + if err != nil { + diags = append(diags, fwdiag.FromErrF(err, "Login Failed, path: auth_flow.%d.basic", index)...) + continue + } return token, nil - } - diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Failed to login with basic auth", - Detail: err.Error(), - AttributePath: cty.GetAttrPath("basic_auth"), - }) - } - if config.AwsAuth != nil { - token, err := authClient.LoginWithAws(ctx, config.AwsAuth.ClusterName, defaultString(config.AwsAuth.AccessKey, ""), defaultString(config.AwsAuth.SecretAccessKey, ""), defaultString(config.AwsAuth.SessionToken, ""), defaultString(config.AwsAuth.Region, ""), defaultString(config.AwsAuth.Profile, "")) - if err == nil { + } else if awsAuth, ok := authConfig.(*AwsAuth); ok { + var clusterName string + if awsAuth.ClusterName == "" { + awsConfig, err := authClient.GetAwsConfiguration(ctx) + if err != nil { + diags = append(diags, fwdiag.FromErrF(err, "Login Failed, path: auth_flow.%d.aws", index)...) + continue + } + clusterName = awsConfig.ValidClusterNames[0] + } else { + clusterName = awsAuth.ClusterName + } + + awsOptions := &bluechip_authenticator.AwsOptions{ + ClusterName: clusterName, + AccessKey: awsAuth.AccessKey, + SecretAccessKey: awsAuth.SecretAccessKey, + SessionToken: awsAuth.SessionToken, + Region: awsAuth.Region, + Profile: awsAuth.Profile, + } + token, err := authClient.LoginWithAws(ctx, awsOptions) + if err != nil { + diags = append(diags, fwdiag.FromErrF(err, "Login Failed, path: auth_flow.%d.aws", index)...) + continue + } return token, nil - } - diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Failed to login with aws auth", - Detail: err.Error(), - AttributePath: cty.GetAttrPath("aws_auth"), - }) - } - if config.OidcAuth != nil { - token, err := authClient.LoginWithOidc(ctx, config.OidcAuth.Token, config.OidcAuth.ValidatorName) - if err == nil { + } else if oidcAuth, ok := authConfig.(*OidcAuth); ok { + var oidcToken string + if oidcAuth.TokenEnv != nil { + oidcToken = os.Getenv(*oidcAuth.TokenEnv) + } + if oidcAuth.Token != nil { + oidcToken = *oidcAuth.Token + } + if oidcToken == "" { + diags = append(diags, diag.Errorf("Failed to resolve oidc token, tokenEnv: %+v, token: %+v, path: auth_flow.%d.oidc", oidcAuth.TokenEnv, oidcAuth.Token, index)...) + continue + } + + token, err := authClient.LoginWithOidc(ctx, oidcToken, oidcAuth.ValidatorName) + if err != nil { + diags = append(diags, fwdiag.FromErrF(err, "Login Failed, path: auth_flow.%d.oidc", index)...) + continue + } return token, nil } - diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Failed to login with oidc auth", - Detail: err.Error(), - AttributePath: cty.GetAttrPath("oidc_auth"), - }) } if len(diags) > 0 { return "", diags } else { - return "", diag.Errorf("either token, basic_auth, aws_auth or oidc_auth must be specified") - } -} - -func defaultString(s *string, def string) string { - if s == nil { - return def + return "", diag.Errorf("either token, basic, aws or oidc must be specified") } - return *s } var resourceMap = map[string]*schema.Resource{} diff --git a/internal/provider/type.go b/internal/provider/type.go new file mode 100644 index 0000000..3a6036f --- /dev/null +++ b/internal/provider/type.go @@ -0,0 +1,210 @@ +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pubg/terraform-provider-bluechip/pkg/framework/fwflex" + "github.com/pubg/terraform-provider-bluechip/pkg/framework/fwtype" +) + +var _ fwtype.TypeHelper[ProviderAuths] = &ProviderAuthType{} + +type ProviderAuths struct { + Items []ProviderAuth +} + +type ProviderAuth interface { + _ProviderAuth() +} + +type TokenAuth struct { + ProviderAuth + + Token string +} + +type BasicAuth struct { + ProviderAuth + + Username string + Password string +} + +type AwsAuth struct { + ProviderAuth + + ClusterName string + AccessKey string + SecretAccessKey string + SessionToken string + Region string + Profile string +} + +type OidcAuth struct { + ProviderAuth + + ValidatorName string + Token *string + TokenEnv *string +} + +type ProviderAuthType struct { +} + +func (t ProviderAuthType) Schema() *schema.Schema { + innerSchema := map[string]*schema.Schema{ + "token": { + Type: schema.TypeList, + Optional: true, + MinItems: 0, + MaxItems: 1, + Description: "Only one of token, basic, aws, oidc can be specified.", + Elem: &schema.Resource{Schema: map[string]*schema.Schema{ + "token": { + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("BLUECHIP_TOKEN", ""), + }, + }}, + }, + "basic": { + Type: schema.TypeList, + Optional: true, + MinItems: 0, + MaxItems: 1, + Description: "Only one of token, basic, aws, oidc can be specified.", + Elem: &schema.Resource{Schema: map[string]*schema.Schema{ + "username": { + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("BLUECHIP_USERNAME", ""), + }, + "password": { + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("BLUECHIP_PASSWORD", ""), + }, + }}, + }, + "aws": { + Type: schema.TypeList, + Optional: true, + MinItems: 0, + MaxItems: 1, + Description: "Only one of token, basic, aws, oidc can be specified.", + Elem: &schema.Resource{Schema: map[string]*schema.Schema{ + "cluster_name": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("BLUECHIP_AWS_CLUSTER_NAME", ""), + }, + "access_key": { + Type: schema.TypeString, + Optional: true, + }, + "secret_access_key": { + Type: schema.TypeString, + Optional: true, + }, + "session_token": { + Type: schema.TypeString, + Optional: true, + }, + "region": { + Type: schema.TypeString, + Optional: true, + }, + "profile": { + Type: schema.TypeString, + Optional: true, + }, + }}, + }, + "oidc": { + Type: schema.TypeList, + Optional: true, + MinItems: 0, + MaxItems: 1, + Description: "Only one of token, basic, aws, oidc can be specified.", + Elem: &schema.Resource{Schema: map[string]*schema.Schema{ + "validator_name": { + Type: schema.TypeString, + Required: true, + }, + "token": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("BLUECHIP_OIDC_TOKEN", ""), + }, + "token_env": { + Type: schema.TypeString, + Optional: true, + Default: "", + }, + }}, + }, + } + + blockSchema := &schema.Schema{ + Type: schema.TypeList, + Required: true, + MinItems: 0, + Elem: &schema.Resource{Schema: innerSchema}, + } + return blockSchema +} + +func (t ProviderAuthType) Expand(ctx context.Context, d *schema.ResourceData, out *ProviderAuths) diag.Diagnostics { + authFlowAttrs := fwflex.ExpandMapList(d.Get("auth_flow").([]any)) + for _, authFlowAttr := range authFlowAttrs { + var count = 0 + if rawAttr, ok := authFlowAttr["token"]; ok && len(rawAttr.([]any)) > 0 { + attr := fwflex.ExpandMapList(rawAttr.([]any))[0] + out.Items = append(out.Items, &TokenAuth{ + Token: attr["token"].(string), + }) + count++ + } + if rawAttr, ok := authFlowAttr["basic"]; ok && len(rawAttr.([]any)) > 0 { + attr := fwflex.ExpandMapList(rawAttr.([]any))[0] + out.Items = append(out.Items, &BasicAuth{ + Username: attr["username"].(string), + Password: attr["password"].(string), + }) + count++ + } + if rawAttr, ok := authFlowAttr["aws"]; ok && len(rawAttr.([]any)) > 0 { + attr := fwflex.ExpandMapList(rawAttr.([]any))[0] + out.Items = append(out.Items, &AwsAuth{ + ClusterName: fwflex.ExpandStringFromMap(attr, "cluster_name"), + AccessKey: fwflex.ExpandStringFromMap(attr, "access_key"), + SecretAccessKey: fwflex.ExpandStringFromMap(attr, "secret_access_key"), + SessionToken: fwflex.ExpandStringFromMap(attr, "session_token"), + Region: fwflex.ExpandStringFromMap(attr, "region"), + Profile: fwflex.ExpandStringFromMap(attr, "profile"), + }) + count++ + } + if rawAttr, ok := authFlowAttr["oidc"]; ok && len(rawAttr.([]any)) > 0 { + attr := fwflex.ExpandMapList(rawAttr.([]any))[0] + out.Items = append(out.Items, &OidcAuth{ + ValidatorName: attr["validator_name"].(string), + Token: fwflex.ExpandStringPFromMap(attr, "token"), + TokenEnv: fwflex.ExpandStringPFromMap(attr, "token_env"), + }) + count++ + } + + if count >= 2 || count == 0 { + return diag.Errorf("Only one of token, basic, aws, oidc can be specified. There are %d of auth blocks specified", count) + } + } + return nil +} + +func (t ProviderAuthType) Flatten(in ProviderAuths) map[string]any { + return nil +} diff --git a/internal/services/applications/whoami_data_source_test.go b/internal/services/applications/whoami_data_source_test.go index 012c552..b85f13d 100644 --- a/internal/services/applications/whoami_data_source_test.go +++ b/internal/services/applications/whoami_data_source_test.go @@ -48,10 +48,12 @@ func TestProviderOidc(t *testing.T) { const TestAccProviderOidcConfig = ` provider "bluechip" { - address = "" - oidc_auth { - validator_name = "gitlab" - token = "asdf" + address = "https://bluechip.example.io" + auth_flow { + oidc { + validator_name = "kubernetes-centre" + token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + } } } ` @@ -75,15 +77,82 @@ func TestProviderAws(t *testing.T) { const TestAccProviderAwsConfig = ` provider "bluechip" { - address = "" - basic_auth { - username = "admin" - password = "admin" + address = "https://bluechip.example.io" + auth_flow { + aws { + cluster_name = "bluechip" + region = "us-east-1" + } } - aws_auth { - cluster_name = "bluechip" - profile = "pubg-devops2" - region = "ap-northeast-2" +} +` + +func TestProviderAwsAutoDiscovery(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testacc.TestAccPreCheck(t) }, + ProtoV5ProviderFactories: testacc.TestAccProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: TestAccProviderAwsAutoDiscoveryConfig + "\n" + TestAccWhoAmiDataSourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.bluechip_whoami.current", "id", "admin"), + resource.TestCheckResourceAttr("data.bluechip_whoami.current", "name", "admin"), + resource.TestCheckResourceAttr("data.bluechip_whoami.current", "groups.0", "system-admin"), + ), + }, + }, + }) +} + +const TestAccProviderAwsAutoDiscoveryConfig = ` +provider "bluechip" { + address = "https://bluechip.example.io" + auth_flow { + aws { + region = "us-east-1" + # profile = "pubg" + } + } +} +` + +func TestProviderChain(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testacc.TestAccPreCheck(t) }, + ProtoV5ProviderFactories: testacc.TestAccProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: TestAccProviderChainConfig + "\n" + TestAccWhoAmiDataSourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.bluechip_whoami.current", "id", "admin"), + resource.TestCheckResourceAttr("data.bluechip_whoami.current", "name", "admin"), + resource.TestCheckResourceAttr("data.bluechip_whoami.current", "groups.0", "system-admin"), + ), + }, + }, + }) +} + +const TestAccProviderChainConfig = ` +provider "bluechip" { + address = "https://bluechip.example.io" + auth_flow { + basic { + username = "aaaaa" + password = "fooooo" + } + } + auth_flow { + oidc { + validator_name = "kubernetes-centre" + token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + } + } + auth_flow { + aws { + cluster_name = "bluechip2" + region = "us-east-1" + } } } ` diff --git a/internal/testacc/provider.go b/internal/testacc/provider.go index 7b5df27..3efb3d2 100644 --- a/internal/testacc/provider.go +++ b/internal/testacc/provider.go @@ -17,7 +17,7 @@ import ( // reattach. var TestAccProtoV5ProviderFactories = map[string]func() (tfprotov5.ProviderServer, error){ "bluechip": func() (tfprotov5.ProviderServer, error) { - return schema.NewGRPCProviderServer(provider.Provider()), nil + return schema.NewGRPCProviderServer(provider.Provider("testacc", "testcommit")()), nil }, } @@ -44,9 +44,11 @@ func ProviderTerraformConfig() string { config := ` provider "bluechip" { address = "$address" - basic_auth { - username = "$username" - password = "$password" + auth_flow { + basic { + username = "$username" + password = "$password" + } } } ` diff --git a/main.go b/main.go index 338ab56..1fe84ff 100644 --- a/main.go +++ b/main.go @@ -26,8 +26,8 @@ import ( //go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs var ( - _ string = "dev" - _ string = "local" + version string = "dev" + commit string = "local" ) func main() { @@ -39,7 +39,7 @@ func main() { opts := &plugin.ServeOpts{ Debug: debugMode, ProviderAddr: "registry.terraform.io/pubg/bluechip", - ProviderFunc: provider.Provider, + ProviderFunc: provider.Provider(version, commit), } plugin.Serve(opts) diff --git a/pkg/bluechip_authenticator/auth.go b/pkg/bluechip_authenticator/auth.go index becba3c..b4979d0 100644 --- a/pkg/bluechip_authenticator/auth.go +++ b/pkg/bluechip_authenticator/auth.go @@ -35,7 +35,7 @@ func (c *Client) doLogin(req *http.Request) (*LoginResponse, *http.Response, err if resp.StatusCode/100 != 2 { bodyBuf := bluechip_client.ReadBodyForError(resp) tflog.Debug(context.Background(), "Login failed", fwlog.Field("status_code", resp.StatusCode), fwlog.Field("body", string(bodyBuf)), fwlog.Field("request", req)) - return nil, resp, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(bodyBuf)) + return nil, resp, fmt.Errorf("unexpected status code: %d, url: %s, body: %s", resp.StatusCode, req.URL.String(), string(bodyBuf)) } var loginResponse LoginResponse @@ -62,33 +62,45 @@ func (c *Client) LoginWithBasic(ctx context.Context, username string, password s return lr.Token, nil } -func (c *Client) LoginWithAws(ctx context.Context, clusterName string, accessKey string, secretAccessKey string, sessionToken string, region string, profile string) (string, error) { - var configLoadOoptions []func(*config.LoadOptions) error - if accessKey != "" && secretAccessKey != "" { - configLoadOoptions = append(configLoadOoptions, config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKey, secretAccessKey, sessionToken))) - tflog.Debug(ctx, "Using credentials from provider parameters") +type AwsOptions struct { + ClusterName string + AccessKey string + SecretAccessKey string + SessionToken string + Region string + Profile string +} + +func (c *Client) LoginWithAws(ctx context.Context, options *AwsOptions) (string, error) { + var configLoadOptions []func(*config.LoadOptions) error + if options.AccessKey != "" && options.SecretAccessKey != "" { + configLoadOptions = append(configLoadOptions, config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(options.AccessKey, options.SecretAccessKey, options.SessionToken))) + tflog.Debug(ctx, "Using static credentials from provider parameters") } - if region != "" { - configLoadOoptions = append(configLoadOoptions, config.WithRegion(region)) + if options.Region != "" { + configLoadOptions = append(configLoadOptions, config.WithRegion(options.Region)) tflog.Debug(ctx, "Using region from provider parameters") } - if profile != "" { - configLoadOoptions = append(configLoadOoptions, config.WithSharedConfigProfile(profile)) + if options.Profile != "" { + configLoadOptions = append(configLoadOptions, config.WithSharedConfigProfile(options.Profile)) tflog.Debug(ctx, "Using profile from provider parameters") } - cfg, err := config.LoadDefaultConfig(ctx, configLoadOoptions...) + cfg, err := config.LoadDefaultConfig(ctx, configLoadOptions...) if err != nil { return "", err } + if cfg.Region == "" { + return "", fmt.Errorf("cannot determine region from provider parameters or aws config") + } tflog.Debug(ctx, fmt.Sprintf("Loaded aws config: %v", cfg)) stsclient := sts.NewFromConfig(cfg) presignclient := sts.NewPresignClient(stsclient) - stsRetriver := NewSTSTokenRetriver(presignclient) - eksToken, err := stsRetriver.GetToken(ctx, clusterName) + stsRetriver := NewSTSTokenRetriever(presignclient) + eksToken, err := stsRetriver.GetToken(ctx, options.ClusterName) if err != nil { return "", err } @@ -127,8 +139,39 @@ func (c *Client) LoginWithOidc(ctx context.Context, token string, authMethod str return lr.Token, nil } +func (c *Client) GetAwsConfiguration(ctx context.Context) (*AwsAuthConfiguration, error) { + u, err := url.JoinPath(c.address, "/discovery/aws-auth-configuration") + if err != nil { + return nil, err + } + + req, _ := http.NewRequestWithContext(ctx, "GET", u, nil) + req.Header.Set("User-Agent", "bluechip-go-http/"+c.version) + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode/100 != 2 { + bodyBuf := bluechip_client.ReadBodyForError(resp) + return nil, fmt.Errorf("unexpected status code: %d, url: %s, body: %s", resp.StatusCode, u, string(bodyBuf)) + } + + var awsAuthConfiguration AwsAuthConfiguration + if err := json.NewDecoder(resp.Body).Decode(&awsAuthConfiguration); err != nil { + return nil, err + } + + return &awsAuthConfiguration, nil +} + type LoginResponse struct { Token string `json:"token"` ExpiresAt string `json:"expiresAt"` Message string `json:"message,omitempty"` } + +type AwsAuthConfiguration struct { + ValidClusterNames []string `json:"validClusterNames"` + ValidTokenPrefixes []string `json:"validTokenPrefixes"` + ClusterIdHeader string `json:"clusterIdHeader"` +} diff --git a/pkg/bluechip_authenticator/aws.go b/pkg/bluechip_authenticator/aws.go index eb7e029..ab4ff40 100644 --- a/pkg/bluechip_authenticator/aws.go +++ b/pkg/bluechip_authenticator/aws.go @@ -23,7 +23,7 @@ type STSTokenRetriever struct { PresignClient *sts.PresignClient } -func NewSTSTokenRetriver(client *sts.PresignClient) STSTokenRetriever { +func NewSTSTokenRetriever(client *sts.PresignClient) STSTokenRetriever { return STSTokenRetriever{PresignClient: client} } diff --git a/pkg/framework/fwdiag/diag.go b/pkg/framework/fwdiag/diag.go new file mode 100644 index 0000000..2b79d03 --- /dev/null +++ b/pkg/framework/fwdiag/diag.go @@ -0,0 +1,21 @@ +package fwdiag + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" +) + +func FromErr(err error, summary string) diag.Diagnostics { + return diag.Diagnostics{ + { + Severity: diag.Error, + Summary: summary, + Detail: err.Error(), + }, + } +} + +func FromErrF(err error, format string, args ...any) diag.Diagnostics { + return FromErr(err, fmt.Sprintf(format, args...)) +} diff --git a/pkg/framework/fwflex/expander.go b/pkg/framework/fwflex/expander.go index 5ee2630..5d43293 100644 --- a/pkg/framework/fwflex/expander.go +++ b/pkg/framework/fwflex/expander.go @@ -64,3 +64,18 @@ func ExpandStringSet(set *schema.Set) []string { } return l } + +func ExpandStringFromMap(m map[string]any, key string) string { + if v, ok := m[key]; ok { + return v.(string) + } + return "" +} + +func ExpandStringPFromMap(m map[string]any, key string) *string { + if v, ok := m[key]; ok { + vv := v.(string) + return &vv + } + return nil +}