diff --git a/go.mod b/go.mod index f341cfc..7791998 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 github.com/humanitec/humanitec-go-autogen v0.0.0-20240429100802-283cee98d746 github.com/stretchr/testify v1.9.0 + sigs.k8s.io/yaml v1.4.0 ) require ( diff --git a/go.sum b/go.sum index c3dcc13..e806ed7 100644 --- a/go.sum +++ b/go.sum @@ -63,6 +63,7 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -300,3 +301,5 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/provider/provider.go b/internal/provider/provider.go index a2346f2..b5effa5 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -3,6 +3,7 @@ package provider import ( "context" "crypto/tls" + "errors" "net/http" "os" @@ -12,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/humanitec/humanitec-go-autogen" + "sigs.k8s.io/yaml" ) // Ensure HumanitecProvider satisfies various provider interfaces. @@ -27,9 +29,11 @@ type HumanitecProvider struct { // HumanitecProviderModel describes the provider data model. type HumanitecProviderModel struct { - Host types.String `tfsdk:"host"` - OrgID types.String `tfsdk:"org_id"` - Token types.String `tfsdk:"token"` + APIPrefix types.String `tfsdk:"api_prefix"` + Host types.String `tfsdk:"host"` + OrgID types.String `tfsdk:"org_id"` + Token types.String `tfsdk:"token"` + Config types.String `tfsdk:"config"` DisableSSLCertificateVerification types.Bool `tfsdk:"disable_ssl_certificate_verification"` } @@ -51,9 +55,14 @@ func (p *HumanitecProvider) Schema(ctx context.Context, req provider.SchemaReque MarkdownDescription: "Terraform Provider for [Humanitec](https://humanitec.com/).", Attributes: map[string]schema.Attribute{ + "api_prefix": schema.StringAttribute{ + MarkdownDescription: "Humanitec API prefix (or using the `HUMANITEC_API_PREFIX` environment variable)", + Optional: true, + }, "host": schema.StringAttribute{ MarkdownDescription: "Humanitec API host (or using the `HUMANITEC_HOST` environment variable)", Optional: true, + DeprecationMessage: "This attribute is deprecated in favor of api_prefix (`HUMANITEC_API_PREFIX` environment variable).", }, "org_id": schema.StringAttribute{ MarkdownDescription: "Humanitec Organization ID (or using the `HUMANITEC_ORG` environment variable)", @@ -68,20 +77,15 @@ func (p *HumanitecProvider) Schema(ctx context.Context, req provider.SchemaReque MarkdownDescription: "Disables SSL certificate verification", Optional: true, }, + "config": schema.StringAttribute{ + MarkdownDescription: "Location of Humanitec configuration", + Optional: true, + }, }, } } func (p *HumanitecProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { - // Check environment variables - host := os.Getenv("HUMANITEC_HOST") - if host == "" { - host = humanitec.DefaultAPIHost - } - - orgID := os.Getenv("HUMANITEC_ORG") - token := os.Getenv("HUMANITEC_TOKEN") - var data HumanitecProviderModel resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) @@ -90,14 +94,100 @@ func (p *HumanitecProvider) Configure(ctx context.Context, req provider.Configur return } + var c Config + + // Check for .humctl file generated by humctl command line tool + configFilePath := data.Config.ValueString() + if configFilePath == "" { + homeDir, err := os.UserHomeDir() + if err != nil { + resp.Diagnostics.AddWarning( + "Unable to determine home directory", + "While configuring the provider, terraform was unable "+ + "to determine user's home directory to read config file.", + ) + configFilePath = "" + } else { + configFilePath = homeDir + "/.humctl" + if _, err := os.Stat(configFilePath); errors.Is(err, os.ErrNotExist) { + configFilePath = "" + } + } + + } else if _, err := os.Stat(configFilePath); errors.Is(err, os.ErrNotExist) { + resp.Diagnostics.AddError( + "Unable to read config file", + "Terraform was unable to read config file mentioned "+ + "in the config attribute.", + ) + // Not returning early allows the logic to collect all errors. + configFilePath = "" + } + + if configFilePath != "" { + file, err := os.ReadFile(configFilePath) + if err != nil { + resp.Diagnostics.AddError( + "Unable to read config file", + "Terraform was unable to read the yaml config file "+ + "in "+configFilePath, + ) + // Not returning early allows the logic to collect all errors. + } + + err = yaml.Unmarshal(file, &c) + if err != nil { + resp.Diagnostics.AddError( + "Unable to parse yaml from config file", + "Terraform was unable to parse yaml config "+ + "file in "+configFilePath, + ) + // Not returning early allows the logic to collect all errors. + } + } + apiPrefix := c.ApiPrefix + orgID := c.Org + token := c.Token + + // Environment variables have precedence over config file, if found + if os.Getenv("HUMANITEC_API_PREFIX") != "" { + apiPrefix = os.Getenv("HUMANITEC_API_PREFIX") + } else { + if hostOld := os.Getenv("HUMANITEC_HOST"); hostOld != "" { + apiPrefix = hostOld + resp.Diagnostics.AddWarning( + "Environment variable HUMANITEC_HOST has been deprecated", + "Environment variable HUMANITEC_HOST has been deprecated "+ + "please use HUMANITEC_API_PREFIX instead to set your api prefix to the terraform driver.") + } else { + apiPrefix = humanitec.DefaultAPIHost + } + } + + if os.Getenv("HUMANITEC_ORG") != "" { + orgID = os.Getenv("HUMANITEC_ORG") + } + + if os.Getenv("HUMANITEC_TOKEN") != "" { + token = os.Getenv("HUMANITEC_TOKEN") + } + // Check configuration data, which should take precedence over - // environment variable data, if found. - if data.Host.ValueString() != "" { - host = data.Host.ValueString() + // environment variable data and config file, if found. + if data.APIPrefix.ValueString() != "" { + apiPrefix = data.APIPrefix.ValueString() + } else if data.Host.ValueString() != "" { + apiPrefix = data.Host.ValueString() + resp.Diagnostics.AddWarning( + "Attribute host has been deprecated", + "Attribute hostT has been deprecated "+ + "please use api_prefix instead to set your api prefix to the terraform driver.") } + if data.OrgID.ValueString() != "" { orgID = data.OrgID.ValueString() } + if data.Token.ValueString() != "" { token = data.Token.ValueString() } @@ -132,7 +222,7 @@ func (p *HumanitecProvider) Configure(ctx context.Context, req provider.Configur doer = &http.Client{} } - client, err := NewHumanitecClient(host, token, p.version, doer) + client, err := NewHumanitecClient(apiPrefix, token, p.version, doer) if err != nil { resp.Diagnostics.AddError("Unable to create Humanitec client", err.Error()) } diff --git a/internal/provider/users_data_source.go b/internal/provider/users_data_source.go index 635fbd7..9d7b97b 100644 --- a/internal/provider/users_data_source.go +++ b/internal/provider/users_data_source.go @@ -22,15 +22,15 @@ func NewUsersDataSource() datasource.DataSource { return &UsersDataSource{} } -// SourceIPRangesDataSource defines the data source implementation. +// UsersDataSource defines the data source implementation. type UsersDataSource struct { client *humanitec.Client orgId string } -// SourceIPRangesDataSourceModel describes the data source data model. +// UsersDataSourceModel describes the data source data model. type UsersDataSourceModel struct { - ID types.String `tfsdk:"id"` + ID types.String `tfsdk:"id"` Filter types.Object `tfsdk:"filter"` Users types.List `tfsdk:"users"` } @@ -173,7 +173,7 @@ func matchesFilters(ctx context.Context, filter basetypes.ObjectValue, userRole diags := filter.As(ctx, &parsedFilter, basetypes.ObjectAsOptions{}) if len(diags) != 0 { return false, diags - } + } id = parsedFilter.Id.ValueStringPointer() name = parsedFilter.Name.ValueStringPointer() @@ -185,4 +185,4 @@ func matchesFilters(ctx context.Context, filter basetypes.ObjectValue, userRole matchesEmailIfSet := email == nil || (userRole.Email != nil && *userRole.Email == *email) return matchesIdIfSet && matchesNameIfSet && matchesEmailIfSet, diag.Diagnostics{} -} \ No newline at end of file +} diff --git a/internal/provider/utils.go b/internal/provider/utils.go index 8eb33c0..e0eb7d2 100644 --- a/internal/provider/utils.go +++ b/internal/provider/utils.go @@ -1,5 +1,11 @@ package provider +type Config struct { + ApiPrefix string `json:"apiPrefix,omitempty"` + Org string `json:"org,omitempty"` + Token string `json:"token,omitempty"` +} + func valueAtPath[T any](input map[string]interface{}, path []string) (T, bool) { lenPath := len(path)