diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index f317c4b..c603cbd 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -238,7 +238,7 @@ jobs: if: ${{ needs.e2e.outputs.status == 'failure' }} steps: - name: Report Failure on slack - if: ${{ needs.e2e.outputs.status == 'failure' }} + if: ${{ github.ref_name == 'main' && needs.e2e.outputs.status == 'failure' }} uses: ravsamhq/notify-slack-action@v2 with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/docs/index.md b/docs/index.md index 37a6b6b..9537622 100644 --- a/docs/index.md +++ b/docs/index.md @@ -85,5 +85,6 @@ provider "clickhouse" { - `api_url` (String) API URL of the ClickHouse OpenAPI the provider will interact with. Alternatively, can be configured using the `CLICKHOUSE_API_URL` environment variable. Only specify if you have a specific deployment of the ClickHouse OpenAPI you want to run against. - `organization_id` (String) ID of the organization the provider will create services under. Alternatively, can be configured using the `CLICKHOUSE_ORG_ID` environment variable. +- `timeout_seconds` (Number) Timeout in seconds for the HTTP client. - `token_key` (String) Token key of the key/secret pair. Used to authenticate with OpenAPI. Alternatively, can be configured using the `CLICKHOUSE_TOKEN_KEY` environment variable. - `token_secret` (String, Sensitive) Token secret of the key/secret pair. Used to authenticate with OpenAPI. Alternatively, can be configured using the `CLICKHOUSE_TOKEN_SECRET` environment variable. diff --git a/pkg/internal/api/client.go b/pkg/internal/api/client.go index 827c233..165a201 100644 --- a/pkg/internal/api/client.go +++ b/pkg/internal/api/client.go @@ -1,6 +1,7 @@ package api import ( + "fmt" "net/http" "time" ) @@ -13,15 +14,39 @@ type ClientImpl struct { TokenSecret string } -func NewClient(apiUrl string, organizationId string, tokenKey string, tokenSecret string) (*ClientImpl, error) { +type ClientConfig struct { + ApiURL string + OrganizationID string + TokenKey string + TokenSecret string + Timeout time.Duration +} + +func NewClient(config ClientConfig) (*ClientImpl, error) { + if config.ApiURL == "" { + return nil, fmt.Errorf("ApiURL cannot be empty") + } + if config.OrganizationID == "" { + return nil, fmt.Errorf("OrganizationID cannot be empty") + } + if config.TokenKey == "" { + return nil, fmt.Errorf("TokenKey cannot be empty") + } + if config.TokenSecret == "" { + return nil, fmt.Errorf("TokenSecret cannot be empty") + } + if config.Timeout == 0 { + config.Timeout = time.Minute * 5 + } + client := &ClientImpl{ - BaseUrl: apiUrl, + BaseUrl: config.ApiURL, HttpClient: &http.Client{ - Timeout: time.Minute * 5, + Timeout: config.Timeout, }, - OrganizationId: organizationId, - TokenKey: tokenKey, - TokenSecret: tokenSecret, + OrganizationId: config.OrganizationID, + TokenKey: config.TokenKey, + TokenSecret: config.TokenSecret, } return client, nil diff --git a/pkg/internal/api/client_test.go b/pkg/internal/api/client_test.go index e5eba0c..83d3095 100644 --- a/pkg/internal/api/client_test.go +++ b/pkg/internal/api/client_test.go @@ -12,14 +12,19 @@ func TestNewClient(t *testing.T) { testClient := &ClientImpl{ BaseUrl: "https://api.clickhouse.cloud/v1", HttpClient: &http.Client{ - Timeout: time.Second * 30, + Timeout: time.Minute * 5, }, OrganizationId: "10ead720-7ca1-48c9-aaf7-7230f42b56c0", TokenKey: "dE8jvpSRVurZCLcLZllb", TokenSecret: "4b1dZbh9bFV9uHQ7Aay4vHHbsTL1HkD2CyZyFBlOLc", } - client, err := NewClient(testClient.BaseUrl, testClient.OrganizationId, testClient.TokenKey, testClient.TokenSecret) + client, err := NewClient(ClientConfig{ + ApiURL: testClient.BaseUrl, + OrganizationID: testClient.OrganizationId, + TokenKey: testClient.TokenKey, + TokenSecret: testClient.TokenSecret, + }) if err != nil { t.Fatalf("new client err: %v", err) } diff --git a/pkg/internal/api/common.go b/pkg/internal/api/common.go index 3ca9ae8..0766a9a 100644 --- a/pkg/internal/api/common.go +++ b/pkg/internal/api/common.go @@ -44,8 +44,8 @@ func (c *ClientImpl) getQueryAPIPath(queryAPIBaseUrl string, serviceID string, f } func (c *ClientImpl) doRequest(ctx context.Context, initialReq *http.Request) ([]byte, error) { - debugctx := tflog.SetField(ctx, "method", initialReq.Method) - debugctx = tflog.SetField(debugctx, "URL", initialReq.URL.String()) + debugctx := tflog.SetField(ctx, "request", fmt.Sprintf("%s %s", initialReq.Method, initialReq.URL.String())) + debugctx = tflog.SetField(debugctx, "clientTimeout", c.HttpClient.Timeout.String()) initialReq.SetBasicAuth(c.TokenKey, c.TokenSecret) @@ -96,6 +96,8 @@ func (c *ClientImpl) doRequest(ctx context.Context, initialReq *http.Request) ([ res, err := c.HttpClient.Do(req) if err != nil { + debugctx = tflog.SetField(debugctx, "error", err.Error()) + tflog.Debug(debugctx, "API request failed") return nil, err } defer res.Body.Close() diff --git a/pkg/internal/api/service.go b/pkg/internal/api/service.go index eafc3a6..cfd1c32 100644 --- a/pkg/internal/api/service.go +++ b/pkg/internal/api/service.go @@ -152,7 +152,10 @@ func (c *ClientImpl) UpdateService(ctx context.Context, serviceId string, s Serv func (c *ClientImpl) DeleteService(ctx context.Context, serviceId string) (*Service, error) { service, err := c.GetService(ctx, serviceId) - if err != nil { + if IsNotFound(err) { + // That is what we want + return nil, nil + } else if err != nil { return nil, err } @@ -166,13 +169,19 @@ func (c *ClientImpl) DeleteService(ctx context.Context, serviceId string) (*Serv } _, err = c.doRequest(ctx, req) - if err != nil { + if IsNotFound(err) { + // That is what we want + return nil, nil + } else if err != nil { return nil, err } } err = c.WaitForServiceState(ctx, serviceId, func(state string) bool { return state == StateStopped }, 10*60) - if err != nil { + if IsNotFound(err) { + // That is what we want + return nil, nil + } else if err != nil { return nil, err } @@ -182,7 +191,10 @@ func (c *ClientImpl) DeleteService(ctx context.Context, serviceId string) (*Serv } body, err := c.doRequest(ctx, req) - if err != nil { + if IsNotFound(err) { + // That is what we want + return nil, nil + } else if err != nil { return nil, err } diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 4f68c5d..a9f8e65 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -4,6 +4,7 @@ import ( "context" _ "embed" "os" + "time" upstreamdatasource "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/path" @@ -42,6 +43,7 @@ type clickhouseProviderModel struct { OrganizationID types.String `tfsdk:"organization_id"` TokenKey types.String `tfsdk:"token_key"` TokenSecret types.String `tfsdk:"token_secret"` + TimeoutSeconds types.Int32 `tfsdk:"timeout_seconds"` } // Metadata returns the provider type name. @@ -70,6 +72,10 @@ func (p *clickhouseProvider) Schema(_ context.Context, _ provider.SchemaRequest, Optional: true, Sensitive: true, }, + "timeout_seconds": schema.Int32Attribute{ + Description: "Timeout in seconds for the HTTP client.", + Optional: true, + }, }, MarkdownDescription: providerDescription, } @@ -159,48 +165,69 @@ func (p *clickhouseProvider) Configure(ctx context.Context, req provider.Configu // If any of the expected configurations are missing, return // errors with provider-specific guidance. - if apiUrl == "" { - resp.Diagnostics.AddAttributeError( - path.Root("api_url"), - "Missing ClickHouse OpenAPI API URL", - "The provider cannot create the ClickHouse OpenAPI client: missing or empty value for the API url. "+ - "Set the API url value in the configuration or use the CLICKHOUSE_API_URL environment variable. "+ - "If either is already set, ensure the value is not empty.", - ) + clientConfig := api.ClientConfig{} + + { + if apiUrl == "" { + resp.Diagnostics.AddAttributeError( + path.Root("api_url"), + "Missing ClickHouse OpenAPI API URL", + "The provider cannot create the ClickHouse OpenAPI client: missing or empty value for the API url. "+ + "Set the API url value in the configuration or use the CLICKHOUSE_API_URL environment variable. "+ + "If either is already set, ensure the value is not empty.", + ) + } + + clientConfig.ApiURL = apiUrl } - if organizationId == "" { - resp.Diagnostics.AddAttributeError( - path.Root("organizationId"), - "Missing ClickHouse OpenAPI Organization ID", - "The provider cannot create the ClickHouse OpenAPI client: missing or empty value for the organization id. "+ - "Set the organization_id value in the configuration or use the CLICKHOUSE_ORG_ID environment variable. "+ - "If either is already set, ensure the value is not empty.", - ) + { + if organizationId == "" { + resp.Diagnostics.AddAttributeError( + path.Root("organizationId"), + "Missing ClickHouse OpenAPI Organization ID", + "The provider cannot create the ClickHouse OpenAPI client: missing or empty value for the organization id. "+ + "Set the organization_id value in the configuration or use the CLICKHOUSE_ORG_ID environment variable. "+ + "If either is already set, ensure the value is not empty.", + ) + } + clientConfig.OrganizationID = organizationId } - if tokenKey == "" { - resp.Diagnostics.AddAttributeError( - path.Root("token_key"), - "Missing ClickHouse OpenAPI Token Key", - "The provider cannot create the ClickHouse OpenAPI client: missing or empty value for the token key. "+ - "Set the token_key value in the configuration or use the CLICKHOUSE_TOKEN_KEY environment variable. "+ - "If either is already set, ensure the value is not empty.", - ) + { + if tokenKey == "" { + resp.Diagnostics.AddAttributeError( + path.Root("token_key"), + "Missing ClickHouse OpenAPI Token Key", + "The provider cannot create the ClickHouse OpenAPI client: missing or empty value for the token key. "+ + "Set the token_key value in the configuration or use the CLICKHOUSE_TOKEN_KEY environment variable. "+ + "If either is already set, ensure the value is not empty.", + ) + } + clientConfig.TokenKey = tokenKey } - if tokenSecret == "" { - resp.Diagnostics.AddAttributeError( - path.Root("token_secret"), - "Missing ClickHouse OpenAPI Token Key", - "The provider cannot create the ClickHouse OpenAPI client: missing or empty value for the token secret. "+ - "Set the token_secret value in the configuration or use the CLICKHOUSE_TOKEN_SECRET environment variable. "+ - "If either is already set, ensure the value is not empty.", - ) + { + if tokenSecret == "" { + resp.Diagnostics.AddAttributeError( + path.Root("token_secret"), + "Missing ClickHouse OpenAPI Token Key", + "The provider cannot create the ClickHouse OpenAPI client: missing or empty value for the token secret. "+ + "Set the token_secret value in the configuration or use the CLICKHOUSE_TOKEN_SECRET environment variable. "+ + "If either is already set, ensure the value is not empty.", + ) + } + clientConfig.TokenSecret = tokenSecret + } + + { + if !config.TimeoutSeconds.IsUnknown() && !config.TimeoutSeconds.IsNull() { + clientConfig.Timeout = time.Second * time.Duration(config.TimeoutSeconds.ValueInt32()) + } } // Create a new ClickHouse client using the configuration values - client, err := api.NewClient(apiUrl, organizationId, tokenKey, tokenSecret) + client, err := api.NewClient(clientConfig) if err != nil { resp.Diagnostics.AddError( "Unable to Create ClickHouse OpenAPI Client",