github.com/supabase/cli@v1.168.1/internal/hostnames/common.go (about)

     1  package hostnames
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"strings"
     8  
     9  	"github.com/go-errors/errors"
    10  	"github.com/supabase/cli/internal/utils"
    11  	"github.com/supabase/cli/pkg/api"
    12  )
    13  
    14  func GetCustomHostnameConfig(ctx context.Context, projectRef string) (*api.GetCustomHostnameConfigResponse, error) {
    15  	resp, err := utils.GetSupabase().GetCustomHostnameConfigWithResponse(ctx, projectRef)
    16  	if err != nil {
    17  		return nil, errors.Errorf("failed to get custom hostname: %w", err)
    18  	}
    19  	if resp.JSON200 == nil {
    20  		return nil, errors.New("failed to get custom hostname config; received: " + string(resp.Body))
    21  	}
    22  	return resp, nil
    23  }
    24  
    25  func VerifyCNAME(ctx context.Context, projectRef string, customHostname string) error {
    26  	expectedEndpoint := fmt.Sprintf("%s.", utils.GetSupabaseHost(projectRef))
    27  	cname, err := utils.ResolveCNAME(ctx, customHostname)
    28  	if err != nil {
    29  		return errors.Errorf("expected custom hostname '%s' to have a CNAME record pointing to your project at '%s', but it failed to resolve: %w", customHostname, expectedEndpoint, err)
    30  	}
    31  	if cname != expectedEndpoint {
    32  		return errors.Errorf("expected custom hostname '%s' to have a CNAME record pointing to your project at '%s', but it is currently set to '%s'", customHostname, expectedEndpoint, cname)
    33  	}
    34  	return nil
    35  }
    36  
    37  type RawResponse struct {
    38  	Result struct {
    39  		CustomOriginServer    string `json:"custom_origin_server"`
    40  		OwnershipVerification struct {
    41  			Name  string
    42  			Type  string
    43  			Value string
    44  		} `json:"ownership_verification"`
    45  		Ssl struct {
    46  			ValidationRecords []struct {
    47  				Status   string `json:"status"`
    48  				TxtName  string `json:"txt_name"`
    49  				TxtValue string `json:"txt_value"`
    50  			} `json:"validation_records"`
    51  			ValidationErrors []struct {
    52  				Message string `json:"message"`
    53  			} `json:"validation_errors"`
    54  			Status string `json:"status"`
    55  		}
    56  	} `json:"result"`
    57  }
    58  
    59  func serializeRawOutput(response *api.UpdateCustomHostnameResponse) (string, error) {
    60  	output, err := json.MarshalIndent(response, "", "    ")
    61  	if err != nil {
    62  		return "", errors.Errorf("failed to serialize json: %w", err)
    63  	}
    64  	return string(output), nil
    65  }
    66  
    67  func appendRawOutputIfNeeded(status string, response *api.UpdateCustomHostnameResponse, includeRawOutput bool) string {
    68  	if !includeRawOutput {
    69  		return status
    70  	}
    71  	rawOutput, err := serializeRawOutput(response)
    72  	if err != nil {
    73  		return fmt.Sprintf("%s\nFailed to serialize raw output: %+v\n", status, err)
    74  	}
    75  	return fmt.Sprintf("%s\nRaw output follows:\n%s\n", status, rawOutput)
    76  }
    77  
    78  func TranslateStatus(response *api.UpdateCustomHostnameResponse, includeRawOutput bool) (string, error) {
    79  	if response.Status == api.N5ServicesReconfigured {
    80  		return appendRawOutputIfNeeded(fmt.Sprintf("Custom hostname setup completed. Project is now accessible at %s.", response.CustomHostname), response, includeRawOutput), nil
    81  	}
    82  	if response.Status == api.N4OriginSetupCompleted {
    83  		var res RawResponse
    84  		rawBody, err := json.Marshal(response.Data)
    85  		if err != nil {
    86  			return "", errors.Errorf("failed to serialize body: %w", err)
    87  		}
    88  		err = json.Unmarshal(rawBody, &res)
    89  		if err != nil {
    90  			return "", errors.Errorf("failed to deserialize body: %w", err)
    91  		}
    92  		return appendRawOutputIfNeeded(fmt.Sprintf(`Custom hostname configuration complete, and ready for activation.
    93  
    94  Please ensure that your custom domain is set up as a CNAME record to your Supabase subdomain:
    95  	%s CNAME -> %s`, response.CustomHostname, res.Result.CustomOriginServer), response, includeRawOutput), nil
    96  	}
    97  	if response.Status == api.N2Initiated {
    98  		var res RawResponse
    99  		rawBody, err := json.Marshal(response.Data)
   100  		if err != nil {
   101  			return "", errors.Errorf("failed to serialize body: %w", err)
   102  		}
   103  		err = json.Unmarshal(rawBody, &res)
   104  		if err != nil {
   105  			return "", errors.Errorf("failed to deserialize body: %w", err)
   106  		}
   107  		owner := res.Result.OwnershipVerification
   108  		ssl := res.Result.Ssl.ValidationRecords
   109  		if res.Result.Ssl.Status == "initializing" {
   110  			return appendRawOutputIfNeeded("Custom hostname setup is being initialized; please request re-verification in a few seconds.\n", response, includeRawOutput), nil
   111  		}
   112  		if len(res.Result.Ssl.ValidationErrors) > 0 {
   113  			var errorMessages []string
   114  			for _, valError := range res.Result.Ssl.ValidationErrors {
   115  				if strings.Contains(valError.Message, "caa_error") {
   116  					return appendRawOutputIfNeeded("CAA mismatch; please remove any existing CAA records on your domain, or add one for \"digicert.com\"\n", response, includeRawOutput), nil
   117  				}
   118  				errorMessages = append(errorMessages, valError.Message)
   119  			}
   120  			valErrors := strings.Join(errorMessages, "\n\t- ")
   121  			return appendRawOutputIfNeeded(fmt.Sprintf("SSL validation errors: \n\t- %s\n", valErrors), response, includeRawOutput), nil
   122  		}
   123  		if len(ssl) != 1 {
   124  			return "", errors.Errorf("expected a single SSL verification record, received: %+v", ssl)
   125  		}
   126  		records := ""
   127  		if owner.Name != "" {
   128  			records = fmt.Sprintf("\n\t%s TXT -> %s", owner.Name, owner.Value)
   129  		}
   130  		if ssl[0].TxtName != "" {
   131  			records = fmt.Sprintf("%s\n\t%s TXT -> %s (replace any existing CNAME records)", records, ssl[0].TxtName, ssl[0].TxtValue)
   132  		}
   133  		status := fmt.Sprintf("Custom hostname verification in-progress; please configure the appropriate DNS entries and request re-verification.\n"+
   134  			"Required outstanding validation records: %s\n",
   135  			records)
   136  		return appendRawOutputIfNeeded(status, response, includeRawOutput), nil
   137  	}
   138  	return appendRawOutputIfNeeded("Custom hostname configuration not started.", response, includeRawOutput), nil
   139  }