dbt-labs / terraform-provider-dbtcloud

dbt Cloud Terraform Provider
https://registry.terraform.io/providers/dbt-labs/dbtcloud
MIT License
82 stars 19 forks source link

Handle resources deleted manually in dbt Cloud #138

Closed b-per closed 1 year ago

b-per commented 1 year ago

Currently, when resources are deleted from dbt Cloud manually, the provider raises errors at the next plan/apply. The provider should handle those cases and recognize that resources have been deleted, recreating them if need be.

I worked on a small POC and the following seems to work:

Update client.go:

// Check error messages in the body
type APIError struct {
    Data   interface{} `json:"data"`
    Status struct {
        Code             int    `json:"code"`
        DeveloperMessage string `json:"developer_message"`
        IsSuccess        bool   `json:"is_success"`
        UserMessage      string `json:"user_message"`
    } `json:"status"`
}

// NewClient -
func NewClient(account_id *int, token *string, host_url *string) (*Client, error) {
// same as today
}

func (c *Client) doRequest(req *http.Request) ([]byte, error) {
    req.Header.Add("Accept", "application/json")
    req.Header.Add("Content-Type", "application/json")
    req.Header.Add("Authorization", fmt.Sprintf("Token %s", c.Token))

    res, err := c.HTTPClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer res.Body.Close()

    body, err := ioutil.ReadAll(res.Body)
    if err != nil {
        return nil, err
    }

    if (res.StatusCode == 404 && isResourceNotFoundError(body)) {
        return nil, fmt.Errorf("resource-not-found")
    }

    if (res.StatusCode != http.StatusOK) && (res.StatusCode != 201) {
        return nil, fmt.Errorf("%s url: %s, status: %d, body: %s", req.Method, req.URL, res.StatusCode, body)
    }

    return body, err
}

func isResourceNotFoundError(body []byte) bool {
    var apiErr APIError
    if unmarshalErr := json.Unmarshal([]byte(body), &apiErr); unmarshalErr != nil {
        return false
    }
        // in this case, the body of the error mentions a 404, this is different from a 404 due to a wrong URL
    if apiErr.Status.Code == 404 {
        return true
    }
    return false
}

And then for all the read functions we can use the following (example with webhook):

    webhook, err := c.GetWebhook(webhookId)
    if err != nil {
        if err.Error() == "resource-not-found" {
            d.SetId("")
            return diags
        }
        return diag.FromErr(err)
    }