From 33525da2a9b55756fb042dc4e26b0b6b49c3efcd Mon Sep 17 00:00:00 2001 From: Liam Bigelow Date: Fri, 13 Feb 2026 00:17:25 +1300 Subject: [PATCH 1/2] Cut over to machines API for certs, add ownership record + imports --- go.mod | 2 +- go.sum | 2 + internal/build/imgsrc/flaps_mock_test.go | 117 +++ internal/command/certificates/root.go | 831 ++++++++++++-------- internal/command/deploy/mock_client_test.go | 32 + internal/flapsutil/flaps_client.go | 10 +- internal/inmem/flaps_client.go | 40 +- internal/mock/flaps_client.go | 154 ++-- 8 files changed, 778 insertions(+), 410 deletions(-) diff --git a/go.mod b/go.mod index 71473b38be..23d98acd13 100644 --- a/go.mod +++ b/go.mod @@ -74,7 +74,7 @@ require ( github.com/spf13/pflag v1.0.9 github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.11.1 - github.com/superfly/fly-go v0.3.0 + github.com/superfly/fly-go v0.3.1 github.com/superfly/graphql v0.2.6 github.com/superfly/lfsc-go v0.1.1 github.com/superfly/macaroon v0.3.0 diff --git a/go.sum b/go.sum index f4a42d36ee..a204d23f05 100644 --- a/go.sum +++ b/go.sum @@ -639,6 +639,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/superfly/fly-go v0.3.0 h1:opzCfB5GeDe2AaqZgtPzWps+fW1zAMhISWAyQrhnV+Y= github.com/superfly/fly-go v0.3.0/go.mod h1:2gCFoNR3iUELADGTJtbBoviMa2jlh2vlPK3cKUajOp8= +github.com/superfly/fly-go v0.3.1 h1:XWrxqQOpZhWFP0gpNgKFec1iXxOAwuW7dYWJW+hh8M8= +github.com/superfly/fly-go v0.3.1/go.mod h1:2gCFoNR3iUELADGTJtbBoviMa2jlh2vlPK3cKUajOp8= github.com/superfly/graphql v0.2.6 h1:zppbodNerWecoXEdjkhrqaNaSjGqobhXNlViHFuZzb4= github.com/superfly/graphql v0.2.6/go.mod h1:CVfDl31srm8HnJ9udwLu6hFNUW/P6GUM2dKcG1YQ8jc= github.com/superfly/lfsc-go v0.1.1 h1:dGjLgt81D09cG+aR9lJZIdmonjZSR5zYCi7s54+ZU2Q= diff --git a/internal/build/imgsrc/flaps_mock_test.go b/internal/build/imgsrc/flaps_mock_test.go index 4fb82ecb55..49b66c1f44 100644 --- a/internal/build/imgsrc/flaps_mock_test.go +++ b/internal/build/imgsrc/flaps_mock_test.go @@ -74,6 +74,21 @@ func (mr *MockFlapsClientMockRecorder) AssignIP(ctx, appName, req any) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssignIP", reflect.TypeOf((*MockFlapsClient)(nil).AssignIP), ctx, appName, req) } +// CheckCertificate mocks base method. +func (m *MockFlapsClient) CheckCertificate(ctx context.Context, appName, hostname string) (*fly.CertificateDetailResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CheckCertificate", ctx, appName, hostname) + ret0, _ := ret[0].(*fly.CertificateDetailResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CheckCertificate indicates an expected call of CheckCertificate. +func (mr *MockFlapsClientMockRecorder) CheckCertificate(ctx, appName, hostname any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckCertificate", reflect.TypeOf((*MockFlapsClient)(nil).CheckCertificate), ctx, appName, hostname) +} + // Cordon mocks base method. func (m *MockFlapsClient) Cordon(ctx context.Context, appName, machineID, nonce string) error { m.ctrl.T.Helper() @@ -88,6 +103,21 @@ func (mr *MockFlapsClientMockRecorder) Cordon(ctx, appName, machineID, nonce any return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cordon", reflect.TypeOf((*MockFlapsClient)(nil).Cordon), ctx, appName, machineID, nonce) } +// CreateACMECertificate mocks base method. +func (m *MockFlapsClient) CreateACMECertificate(ctx context.Context, appName string, req fly.CreateCertificateRequest) (*fly.CertificateDetailResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateACMECertificate", ctx, appName, req) + ret0, _ := ret[0].(*fly.CertificateDetailResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateACMECertificate indicates an expected call of CreateACMECertificate. +func (mr *MockFlapsClientMockRecorder) CreateACMECertificate(ctx, appName, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateACMECertificate", reflect.TypeOf((*MockFlapsClient)(nil).CreateACMECertificate), ctx, appName, req) +} + // CreateApp mocks base method. func (m *MockFlapsClient) CreateApp(ctx context.Context, req flaps.CreateAppRequest) (*flaps.App, error) { m.ctrl.T.Helper() @@ -103,6 +133,21 @@ func (mr *MockFlapsClientMockRecorder) CreateApp(ctx, req any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateApp", reflect.TypeOf((*MockFlapsClient)(nil).CreateApp), ctx, req) } +// CreateCustomCertificate mocks base method. +func (m *MockFlapsClient) CreateCustomCertificate(ctx context.Context, appName string, req fly.ImportCertificateRequest) (*fly.CertificateDetailResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateCustomCertificate", ctx, appName, req) + ret0, _ := ret[0].(*fly.CertificateDetailResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateCustomCertificate indicates an expected call of CreateCustomCertificate. +func (mr *MockFlapsClientMockRecorder) CreateCustomCertificate(ctx, appName, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCustomCertificate", reflect.TypeOf((*MockFlapsClient)(nil).CreateCustomCertificate), ctx, appName, req) +} + // CreateVolume mocks base method. func (m *MockFlapsClient) CreateVolume(ctx context.Context, appName string, req fly.CreateVolumeRequest) (*fly.Volume, error) { m.ctrl.T.Helper() @@ -132,6 +177,20 @@ func (mr *MockFlapsClientMockRecorder) CreateVolumeSnapshot(ctx, appName, volume return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateVolumeSnapshot", reflect.TypeOf((*MockFlapsClient)(nil).CreateVolumeSnapshot), ctx, appName, volumeId) } +// DeleteACMECertificate mocks base method. +func (m *MockFlapsClient) DeleteACMECertificate(ctx context.Context, appName, hostname string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteACMECertificate", ctx, appName, hostname) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteACMECertificate indicates an expected call of DeleteACMECertificate. +func (mr *MockFlapsClientMockRecorder) DeleteACMECertificate(ctx, appName, hostname any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteACMECertificate", reflect.TypeOf((*MockFlapsClient)(nil).DeleteACMECertificate), ctx, appName, hostname) +} + // DeleteApp mocks base method. func (m *MockFlapsClient) DeleteApp(ctx context.Context, name string) error { m.ctrl.T.Helper() @@ -161,6 +220,34 @@ func (mr *MockFlapsClientMockRecorder) DeleteAppSecret(ctx, appName, name any) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAppSecret", reflect.TypeOf((*MockFlapsClient)(nil).DeleteAppSecret), ctx, appName, name) } +// DeleteCertificate mocks base method. +func (m *MockFlapsClient) DeleteCertificate(ctx context.Context, appName, hostname string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteCertificate", ctx, appName, hostname) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteCertificate indicates an expected call of DeleteCertificate. +func (mr *MockFlapsClientMockRecorder) DeleteCertificate(ctx, appName, hostname any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCertificate", reflect.TypeOf((*MockFlapsClient)(nil).DeleteCertificate), ctx, appName, hostname) +} + +// DeleteCustomCertificate mocks base method. +func (m *MockFlapsClient) DeleteCustomCertificate(ctx context.Context, appName, hostname string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteCustomCertificate", ctx, appName, hostname) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteCustomCertificate indicates an expected call of DeleteCustomCertificate. +func (mr *MockFlapsClientMockRecorder) DeleteCustomCertificate(ctx, appName, hostname any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCustomCertificate", reflect.TypeOf((*MockFlapsClient)(nil).DeleteCustomCertificate), ctx, appName, hostname) +} + // DeleteIPAssignment mocks base method. func (m *MockFlapsClient) DeleteIPAssignment(ctx context.Context, appName, ip string) error { m.ctrl.T.Helper() @@ -338,6 +425,21 @@ func (mr *MockFlapsClientMockRecorder) GetApp(ctx, name any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetApp", reflect.TypeOf((*MockFlapsClient)(nil).GetApp), ctx, name) } +// GetCertificate mocks base method. +func (m *MockFlapsClient) GetCertificate(ctx context.Context, appName, hostname string) (*fly.CertificateDetailResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCertificate", ctx, appName, hostname) + ret0, _ := ret[0].(*fly.CertificateDetailResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCertificate indicates an expected call of GetCertificate. +func (mr *MockFlapsClientMockRecorder) GetCertificate(ctx, appName, hostname any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCertificate", reflect.TypeOf((*MockFlapsClient)(nil).GetCertificate), ctx, appName, hostname) +} + // GetIPAssignments mocks base method. func (m *MockFlapsClient) GetIPAssignments(ctx context.Context, appName string) (*flaps.ListIPAssignmentsResponse, error) { m.ctrl.T.Helper() @@ -562,6 +664,21 @@ func (mr *MockFlapsClientMockRecorder) ListApps(ctx, req any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListApps", reflect.TypeOf((*MockFlapsClient)(nil).ListApps), ctx, req) } +// ListCertificates mocks base method. +func (m *MockFlapsClient) ListCertificates(ctx context.Context, appName string, opts *flaps.ListCertificatesOpts) (*fly.ListCertificatesResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListCertificates", ctx, appName, opts) + ret0, _ := ret[0].(*fly.ListCertificatesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListCertificates indicates an expected call of ListCertificates. +func (mr *MockFlapsClientMockRecorder) ListCertificates(ctx, appName, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCertificates", reflect.TypeOf((*MockFlapsClient)(nil).ListCertificates), ctx, appName, opts) +} + // ListFlyAppsMachines mocks base method. func (m *MockFlapsClient) ListFlyAppsMachines(ctx context.Context, appName string) ([]*fly.Machine, *fly.Machine, error) { m.ctrl.T.Helper() diff --git a/internal/command/certificates/root.go b/internal/command/certificates/root.go index d9b2018a78..cf2ddc1737 100644 --- a/internal/command/certificates/root.go +++ b/internal/command/certificates/root.go @@ -3,17 +3,17 @@ package certificates import ( "context" "fmt" - "net" + "os" "strings" "github.com/dustin/go-humanize" fly "github.com/superfly/fly-go" + "github.com/superfly/fly-go/flaps" "github.com/superfly/flyctl/internal/appconfig" - "github.com/superfly/flyctl/internal/certificate" "github.com/superfly/flyctl/internal/command" "github.com/superfly/flyctl/internal/config" "github.com/superfly/flyctl/internal/flag" - "github.com/superfly/flyctl/internal/flyutil" + "github.com/superfly/flyctl/internal/flapsutil" "github.com/superfly/flyctl/internal/prompt" "github.com/superfly/flyctl/internal/render" "github.com/superfly/flyctl/iostreams" @@ -34,6 +34,7 @@ certificates issued for the hostname/domain by Let's Encrypt.` cmd.AddCommand( newCertificatesList(), newCertificatesAdd(), + newCertificatesImport(), newCertificatesRemove(), newCertificatesCheck(), newCertificatesSetup(), @@ -79,11 +80,118 @@ as a parameter for the certificate.` return cmd } +func newCertificatesImport() *cobra.Command { + const ( + short = "Import a custom certificate" + long = `Import a custom TLS certificate for a hostname. + +Upload your own certificate and private key in PEM format. Requires domain +ownership verification via DNS before the certificate becomes active.` + ) + cmd := command.New("import ", short, long, runCertificatesImport, + command.RequireSession, + command.RequireAppName, + ) + flag.Add(cmd, + flag.App(), + flag.AppConfig(), + flag.String{ + Name: "fullchain", + Description: "Path to certificate chain file (PEM format)", + }, + flag.String{ + Name: "private-key", + Description: "Path to private key file (PEM format)", + }, + flag.JSONOutput(), + ) + cmd.Args = cobra.ExactArgs(1) + cmd.MarkFlagRequired("fullchain") + cmd.MarkFlagRequired("private-key") + return cmd +} + +func runCertificatesImport(ctx context.Context) error { + flapsClient := flapsutil.ClientFromContext(ctx) + appName := appconfig.NameFromContext(ctx) + hostname := flag.FirstArg(ctx) + + fullchainPath := flag.GetString(ctx, "fullchain") + privateKeyPath := flag.GetString(ctx, "private-key") + + fullchain, err := os.ReadFile(fullchainPath) + if err != nil { + return fmt.Errorf("failed to read certificate file: %w", err) + } + + privateKey, err := os.ReadFile(privateKeyPath) + if err != nil { + return fmt.Errorf("failed to read private key file: %w", err) + } + + resp, err := flapsClient.CreateCustomCertificate(ctx, appName, fly.ImportCertificateRequest{ + Hostname: hostname, + Fullchain: string(fullchain), + PrivateKey: string(privateKey), + }) + if err != nil { + return err + } + + io := iostreams.FromContext(ctx) + colorize := io.ColorScheme() + + if config.FromContext(ctx).JSONOutput { + render.JSON(io.Out, resp) + return nil + } + + fmt.Fprintf(io.Out, "Certificate uploaded for %s\n", colorize.Bold(resp.Hostname)) + + var customCert *fly.CertificateDetail + for i := range resp.Certificates { + if resp.Certificates[i].Source == "custom" { + customCert = &resp.Certificates[i] + break + } + } + + if customCert == nil { + return fmt.Errorf("unexpected response: no custom certificate in response") + } + + switch customCert.Status { + case "pending_ownership": + ov := resp.DNSRequirements.Ownership + fmt.Fprintln(io.Out) + if strings.HasPrefix(hostname, "*.") { + fmt.Fprintf(io.Out, "%s Your custom certificate is uploaded but not yet active. Add a TXT record to verify domain ownership.\n", colorize.WarningIcon()) + } else { + fmt.Fprintf(io.Out, "%s Your custom certificate is uploaded but not yet active. Add a TXT or AAAA record to verify domain ownership.\n", colorize.WarningIcon()) + } + fmt.Fprintln(io.Out) + fmt.Fprintf(io.Out, " %s %s %s\n", ov.Name, colorize.Cyan("TXT"), ov.AppValue) + fmt.Fprintln(io.Out) + fmt.Fprintf(io.Out, "Run %s to verify.\n", colorize.Bold("fly certs check "+quoteHostname(hostname))) + case "active": + fmt.Fprintln(io.Out) + fmt.Fprintf(io.Out, "%s Certificate is active!\n", colorize.SuccessIcon()) + if customCert.ExpiresAt != nil && !customCert.ExpiresAt.IsZero() { + fmt.Fprintf(io.Out, "Expires: %s\n", humanize.Time(*customCert.ExpiresAt)) + } + } + + return nil +} + func newCertificatesRemove() *cobra.Command { const ( short = "Removes a certificate from an app" long = `Removes a certificate from an application. Takes hostname -as a parameter to locate the certificate.` +as a parameter to locate the certificate. + +Use --custom to remove only the custom certificate while keeping ACME certificates. +Use --acme to stop ACME certificate issuance while keeping custom certificates.` ) cmd := command.New("remove ", short, long, runCertificatesRemove, command.RequireSession, @@ -93,6 +201,16 @@ as a parameter to locate the certificate.` flag.App(), flag.AppConfig(), flag.Yes(), + flag.Bool{ + Name: "custom", + Description: "Remove only the custom certificate, keeping ACME certificates", + Default: false, + }, + flag.Bool{ + Name: "acme", + Description: "Stop ACME certificate issuance, keeping custom certificates", + Default: false, + }, ) cmd.Args = cobra.ExactArgs(1) cmd.Aliases = []string{"delete"} @@ -140,82 +258,201 @@ Takes hostname as a parameter to show the setup instructions for that certificat func runCertificatesList(ctx context.Context) error { appName := appconfig.NameFromContext(ctx) - apiClient := flyutil.ClientFromContext(ctx) + flapsClient := flapsutil.ClientFromContext(ctx) - certs, err := apiClient.GetAppCertificates(ctx, appName) + resp, err := flapsClient.ListCertificates(ctx, appName, &flaps.ListCertificatesOpts{Limit: 50}) if err != nil { return err } - return printCertificates(ctx, certs) + if err := printCertificates(ctx, resp.Certificates); err != nil { + return err + } + + if resp.NextCursor != "" { + io := iostreams.FromContext(ctx) + fmt.Fprintf(io.Out, "\nShowing %d of %d certificates. Use the Machines API to paginate through all results.\n", + len(resp.Certificates), resp.TotalCount) + } + + return nil } func runCertificatesCheck(ctx context.Context) error { - apiClient := flyutil.ClientFromContext(ctx) + flapsClient := flapsutil.ClientFromContext(ctx) appName := appconfig.NameFromContext(ctx) hostname := flag.FirstArg(ctx) - cert, hostcheck, err := apiClient.CheckAppCertificate(ctx, appName, hostname) + resp, err := flapsClient.CheckCertificate(ctx, appName, hostname) if err != nil { return err } - printCertificate(ctx, cert) - io := iostreams.FromContext(ctx) colorize := io.ColorScheme() - if cert.ClientStatus == "Ready" { - fmt.Fprintf(io.Out, "\n%s\n", colorize.Green("✓ Your certificate has been issued!")) - fmt.Fprintf(io.Out, "%s\n", colorize.Green("Your DNS is correctly configured and this certificate will auto-renew before expiration.")) + if config.FromContext(ctx).JSONOutput { + render.JSON(io.Out, resp) return nil } - // If certificates were issued but status is not ready, DNS is broken - if len(cert.Issued.Nodes) > 0 { - fmt.Fprintf(io.Out, "\n%s\n", colorize.Yellow("Your certificate was issued but your DNS configuration has issues.")) - fmt.Fprintf(io.Out, "%s\n", colorize.Yellow("This certificate may not renew automatically. Please fix your DNS configuration.")) + printCertificateDetail(ctx, resp) + + if len(resp.ValidationErrors) > 0 { + fmt.Fprintln(io.Out) + for _, ve := range resp.ValidationErrors { + fmt.Fprintf(io.Out, "%s %s\n", colorize.WarningIcon(), colorize.Yellow(ve.Message)) + if ve.Remediation != "" { + fmt.Fprintf(io.Out, " %s\n", ve.Remediation) + } + } + } + + hasActive := false + hasPendingOwnership := false + for _, cert := range resp.Certificates { + if cert.Status == "active" { + hasActive = true + } + if cert.Status == "pending_ownership" { + hasPendingOwnership = true + } } - if len(cert.ValidationErrors) > 0 { - certificate.DisplayValidationErrors(io, cert.ValidationErrors) + fmt.Fprintln(io.Out) + + if hasActive { + fmt.Fprintf(io.Out, "%s %s\n", colorize.SuccessIcon(), colorize.Green("Certificate is verified and active")) + return nil } - return reportNextStepCert(ctx, hostname, cert, hostcheck, DNSDisplaySkip) + if hasPendingOwnership { + ownership := resp.DNSRequirements.Ownership + if ownership.Name != "" { + fmt.Fprintln(io.Out, "Add this DNS record to verify domain ownership:") + fmt.Fprintln(io.Out) + fmt.Fprintf(io.Out, " TXT %s → %s\n", ownership.Name, ownership.AppValue) + fmt.Fprintln(io.Out) + fmt.Fprintf(io.Out, "Run %s after adding the record.\n", colorize.Bold("fly certs check "+quoteHostname(hostname))) + } + return nil + } + + fmt.Fprintf(io.Out, "Run %s to view DNS setup instructions.\n", colorize.Bold("fly certs setup "+quoteHostname(hostname))) + + return nil } func runCertificatesAdd(ctx context.Context) error { - apiClient := flyutil.ClientFromContext(ctx) + flapsClient := flapsutil.ClientFromContext(ctx) appName := appconfig.NameFromContext(ctx) hostname := flag.FirstArg(ctx) - cert, hostcheck, err := apiClient.AddCertificate(ctx, appName, hostname) + resp, err := flapsClient.CreateACMECertificate(ctx, appName, fly.CreateCertificateRequest{ + Hostname: hostname, + }) if err != nil { return err } - err = reportNextStepCert(ctx, hostname, cert, hostcheck, DNSDisplayForce) - if err != nil { - return err + if config.FromContext(ctx).JSONOutput { + io := iostreams.FromContext(ctx) + render.JSON(io.Out, resp) + return nil } + printCertAdded(ctx, hostname, resp) + + return nil +} + +func quoteHostname(hostname string) string { + if strings.Contains(hostname, "*") { + return "'" + hostname + "'" + } + return hostname +} + +func printCertAdded(ctx context.Context, hostname string, resp *fly.CertificateDetailResponse) { io := iostreams.FromContext(ctx) colorize := io.ColorScheme() - fmt.Fprintf(io.Out, "\nOnce your DNS is configured correctly, we will automatically provision your certificate.\n") - fmt.Fprintf(io.Out, "To check progress, run: %s\n", colorize.Bold(fmt.Sprintf("fly certs check '%s'", hostname))) - return nil + var ipV4, ipV6 string + if len(resp.DNSRequirements.A) > 0 { + ipV4 = resp.DNSRequirements.A[0] + } + if len(resp.DNSRequirements.AAAA) > 0 { + ipV6 = resp.DNSRequirements.AAAA[0] + } + + isWildcard := strings.HasPrefix(hostname, "*.") + quoted := quoteHostname(hostname) + + fmt.Fprintf(io.Out, "%s Certificate created for %s\n", colorize.SuccessIcon(), colorize.Bold(hostname)) + fmt.Fprintln(io.Out) + + if ipV4 == "" && ipV6 == "" { + fmt.Fprintf(io.Out, "%s Your app has no public IP addresses.\n", colorize.WarningIcon()) + fmt.Fprintf(io.Out, "Run %s to allocate IPs.\n", colorize.Bold("fly ips allocate")) + } else { + fmt.Fprintln(io.Out, colorize.Bold("Recommended DNS setup:")) + if ipV4 != "" { + fmt.Fprintf(io.Out, " %s %s \u2192 %s\n", colorize.Cyan("A"), hostname, ipV4) + } + if ipV6 != "" { + fmt.Fprintf(io.Out, " %s %s \u2192 %s\n", colorize.Cyan("AAAA"), hostname, ipV6) + } + + if ipV4 == "" { + fmt.Fprintln(io.Out) + fmt.Fprintf(io.Out, "Run %s to add an IPv4 address.\n", colorize.Bold("fly ips allocate")) + } + } + + if isWildcard { + acme := resp.DNSRequirements.ACMEChallenge + if acme.Name != "" && acme.Target != "" { + fmt.Fprintln(io.Out) + fmt.Fprintf(io.Out, "%s Wildcard certificates require DNS validation:\n", colorize.WarningIcon()) + fmt.Fprintf(io.Out, " %s %s \u2192 %s\n", colorize.Cyan("CNAME"), acme.Name, acme.Target) + } + } + + if !isWildcard && needsAlternateHostname(hostname) { + alternateHostname := getAlternateHostname(hostname) + if strings.HasPrefix(alternateHostname, "www.") { + fmt.Fprintln(io.Out) + fmt.Fprintf(io.Out, "%s Run %s to cover the www subdomain.\n", colorize.Gray("Tip:"), colorize.Bold("fly certs add "+alternateHostname)) + } + } + + fmt.Fprintln(io.Out) + fmt.Fprintf(io.Out, "Run %s to check validation progress.\n", colorize.Bold("fly certs check "+quoted)) + fmt.Fprintf(io.Out, "Run %s for alternative DNS setups.\n", colorize.Bold("fly certs setup "+quoted)) } func runCertificatesRemove(ctx context.Context) error { io := iostreams.FromContext(ctx) colorize := io.ColorScheme() - apiClient := flyutil.ClientFromContext(ctx) + flapsClient := flapsutil.ClientFromContext(ctx) appName := appconfig.NameFromContext(ctx) hostname := flag.FirstArg(ctx) + customOnly := flag.GetBool(ctx, "custom") + acmeOnly := flag.GetBool(ctx, "acme") + + if customOnly && acmeOnly { + return fmt.Errorf("cannot specify both --custom and --acme") + } if !flag.GetYes(ctx) { - message := fmt.Sprintf("Remove certificate %s from app %s?", hostname, appName) + var message string + if customOnly { + message = fmt.Sprintf("Remove custom certificate for %s from app %s?", hostname, appName) + } else if acmeOnly { + message = fmt.Sprintf("Stop ACME certificate issuance for %s on app %s?", hostname, appName) + } else { + message = fmt.Sprintf("Remove certificate %s from app %s?", hostname, appName) + } confirm, err := prompt.Confirm(ctx, message) if err != nil { @@ -227,334 +464,267 @@ func runCertificatesRemove(ctx context.Context) error { } } - cert, err := apiClient.DeleteCertificate(ctx, appName, hostname) + var err error + if customOnly { + err = flapsClient.DeleteCustomCertificate(ctx, appName, hostname) + } else if acmeOnly { + err = flapsClient.DeleteACMECertificate(ctx, appName, hostname) + } else { + err = flapsClient.DeleteCertificate(ctx, appName, hostname) + } if err != nil { return err } - fmt.Fprintf(io.Out, "Certificate %s deleted from app %s\n", - colorize.Bold(cert.Certificate.Hostname), - colorize.Bold(cert.App.Name), - ) + if customOnly { + fmt.Fprintf(io.Out, "%s Custom certificate for %s deleted from app %s\n", + colorize.SuccessIcon(), + colorize.Bold(hostname), + colorize.Bold(appName), + ) + } else if acmeOnly { + fmt.Fprintf(io.Out, "%s ACME certificate issuance stopped for %s on app %s\n", + colorize.SuccessIcon(), + colorize.Bold(hostname), + colorize.Bold(appName), + ) + } else { + fmt.Fprintf(io.Out, "%s Certificate %s deleted from app %s\n", + colorize.SuccessIcon(), + colorize.Bold(hostname), + colorize.Bold(appName), + ) + } return nil } func runCertificatesSetup(ctx context.Context) error { - apiClient := flyutil.ClientFromContext(ctx) + flapsClient := flapsutil.ClientFromContext(ctx) appName := appconfig.NameFromContext(ctx) hostname := flag.FirstArg(ctx) - cert, hostcheck, err := apiClient.CheckAppCertificate(ctx, appName, hostname) + resp, err := flapsClient.CheckCertificate(ctx, appName, hostname) if err != nil { return err } - return reportNextStepCert(ctx, hostname, cert, hostcheck, DNSDisplayForce) + printDNSOptions(ctx, hostname, resp) + return nil } -type DNSDisplayMode int - -const ( - DNSDisplayAuto DNSDisplayMode = iota // Show setup steps if required - DNSDisplayForce // Always show setup steps - DNSDisplaySkip // Never show setup steps -) - -func reportNextStepCert(ctx context.Context, hostname string, cert *fly.AppCertificate, hostcheck *fly.HostnameCheck, dnsMode DNSDisplayMode) error { +func printDNSOptions(ctx context.Context, hostname string, resp *fly.CertificateDetailResponse) { io := iostreams.FromContext(ctx) - - // print a blank line, easier to read! - fmt.Fprintln(io.Out) - colorize := io.ColorScheme() - appName := appconfig.NameFromContext(ctx) - apiClient := flyutil.ClientFromContext(ctx) - // These are the IPs we have for the app - ips, err := apiClient.GetIPAddresses(ctx, appName) - if err != nil { - return err + var ipV4, ipV6 string + if len(resp.DNSRequirements.A) > 0 { + ipV4 = resp.DNSRequirements.A[0] } - - cnameTarget, err := apiClient.GetAppCNAMETarget(ctx, appName) - if err != nil { - return err + if len(resp.DNSRequirements.AAAA) > 0 { + ipV6 = resp.DNSRequirements.AAAA[0] } + cnameTarget := resp.DNSRequirements.CNAME - var ipV4 fly.IPAddress - var ipV6 fly.IPAddress - var configuredipV4 bool - var configuredipV6 bool - var externalProxyHint bool - - // Extract the v4 and v6 addresses we have allocated - for _, x := range ips { - switch x.Type { - case "v4", "shared_v4": - ipV4 = x - case "v6": - ipV6 = x + isWildcard := strings.HasPrefix(hostname, "*.") + hasACME := resp.AcmeRequested + hasCustom := false + for _, cert := range resp.Certificates { + if cert.Source == "custom" { + hasCustom = true + } else { + hasACME = true } } + if !hasCustom { + hasACME = true + } - // Do we have A records - if len(hostcheck.ARecords) > 0 { - // Let's check the first A record against our recorded addresses - ip := net.ParseIP(hostcheck.ARecords[0]) - if !ip.Equal(net.ParseIP(ipV4.Address)) { - if isExternalProxied(cert.DNSProvider, ip) { - externalProxyHint = true - } else { - fmt.Fprintf(io.Out, colorize.Yellow("A Record (%s) does not match app's IP (%s)\n"), hostcheck.ARecords[0], ipV4.Address) - } + eTLD, _ := publicsuffix.EffectiveTLDPlusOne(hostname) + isApex := hostname == eTLD + showCNAME := cnameTarget != "" && !isApex + + fmt.Fprintf(io.Out, "%s\n", colorize.Bold(fmt.Sprintf("DNS Setup Options for %s", hostname))) + + hasRoutingOptions := (ipV4 != "" || ipV6 != "") || showCNAME + if hasRoutingOptions { + fmt.Fprintln(io.Out) + if showCNAME { + fmt.Fprintf(io.Out, "%s\n", colorize.Bold("Route traffic to your app with one of:")) } else { - configuredipV4 = true + fmt.Fprintf(io.Out, "%s\n", colorize.Bold("Route traffic to your app:")) } } - if len(hostcheck.AAAARecords) > 0 { - // Let's check the first A record against our recorded addresses - ip := net.ParseIP(hostcheck.AAAARecords[0]) - if !ip.Equal(net.ParseIP(ipV6.Address)) { - if isExternalProxied(cert.DNSProvider, ip) { - externalProxyHint = true - } else { - fmt.Fprintf(io.Out, colorize.Yellow("AAAA Record (%s) does not match app's IP (%s)\n"), hostcheck.AAAARecords[0], ipV6.Address) - } + optionNum := 1 + if ipV4 != "" || ipV6 != "" { + fmt.Fprintln(io.Out) + if showCNAME { + fmt.Fprintf(io.Out, " %s\n", colorize.Green(fmt.Sprintf("%d. A and AAAA Records (recommended)", optionNum))) } else { - configuredipV6 = true + fmt.Fprintf(io.Out, " %s\n", colorize.Green("A and AAAA Records")) } - } - - if len(hostcheck.ResolvedAddresses) > 0 { - for _, address := range hostcheck.ResolvedAddresses { - ip := net.ParseIP(address) - if ip.Equal(net.ParseIP(ipV4.Address)) { - configuredipV4 = true - } else if ip.Equal(net.ParseIP(ipV6.Address)) { - configuredipV6 = true - } else { - if isExternalProxied(cert.DNSProvider, ip) { - externalProxyHint = true - } else { - fmt.Fprintf(io.Out, colorize.Yellow("Address resolution (%s) does not match app's IP (%s/%s)\n"), address, ipV4.Address, ipV6.Address) - } - } + if ipV4 != "" { + fmt.Fprintf(io.Out, " %s %s \u2192 %s\n", colorize.Cyan("A"), hostname, ipV4) + } + if ipV6 != "" { + fmt.Fprintf(io.Out, " %s %s \u2192 %s\n", colorize.Cyan("AAAA"), hostname, ipV6) } + optionNum++ } - var addDNSConfig bool - switch { - case cert.IsApex: - addDNSConfig = !configuredipV4 || !configuredipV6 - case cert.IsWildcard: - addDNSConfig = !configuredipV4 || !cert.AcmeDNSConfigured - default: - nothingConfigured := !(configuredipV4 && configuredipV6) - onlyV4Configured := configuredipV4 && !configuredipV6 - addDNSConfig = nothingConfigured || onlyV4Configured - } - - switch { - case dnsMode == DNSDisplaySkip && addDNSConfig: - fmt.Fprintln(io.Out, "Your DNS is not yet configured correctly.") - fmt.Fprintf(io.Out, "Run %s to view DNS setup instructions.\n", colorize.Bold("fly certs setup "+hostname)) - case dnsMode == DNSDisplayForce || (dnsMode == DNSDisplayAuto && addDNSConfig): - printDNSSetupOptions(DNSSetupFlags{ - Context: ctx, - Hostname: hostname, - Certificate: cert, - IPv4Address: ipV4, - IPv6Address: ipV6, - CNAMETarget: cnameTarget, - ExternalProxyDetected: externalProxyHint, - }) - case cert.ClientStatus == "Ready": - fmt.Fprintf(io.Out, "Your certificate for %s has been issued. \n", hostname) - default: - fmt.Fprintf(io.Out, "Your certificate for %s is being issued. Status is %s. \n", hostname, cert.ClientStatus) + if showCNAME { + fmt.Fprintln(io.Out) + fmt.Fprintf(io.Out, " %s\n", fmt.Sprintf("%d. CNAME Record", optionNum)) + fmt.Fprintf(io.Out, " %s %s \u2192 %s\n", colorize.Cyan("CNAME"), hostname, cnameTarget) } - if dnsMode != DNSDisplaySkip && !cert.IsWildcard && needsAlternateHostname(hostname) { - alternateHostname := getAlternateHostname(hostname) - fmt.Fprintf(io.Out, "Make sure to create another certificate for %s. \n", alternateHostname) + acme := resp.DNSRequirements.ACMEChallenge + showACME := hasACME && acme.Name != "" && acme.Target != "" + + ownership := resp.DNSRequirements.Ownership + showOwnership := ownership.Name != "" && !(hasACME && !hasCustom && isWildcard) + + if showACME || showOwnership { + fmt.Fprintln(io.Out) + fmt.Fprintf(io.Out, "%s\n", colorize.Bold("Additional DNS records:")) } - return nil -} + if showACME { + fmt.Fprintln(io.Out) + fmt.Fprintf(io.Out, " ACME DNS Challenge\n") + fmt.Fprintf(io.Out, " %s %s \u2192 %s\n", colorize.Cyan("CNAME"), acme.Name, acme.Target) + if isWildcard { + fmt.Fprintf(io.Out, " %s\n", colorize.Gray("Required to issue fly-managed wildcard certificates.")) + } else { + fmt.Fprintf(io.Out, " %s\n", colorize.Gray("Only needed if you want to generate the certificate before directing traffic to your application.")) + } + } -func isExternalProxied(provider string, ip net.IP) bool { - if provider == CLOUDFLARE { - for _, ipnet := range CloudflareIPs { - if ipnet.Contains(ip) { - return true - } + if showOwnership { + fmt.Fprintln(io.Out) + fmt.Fprintf(io.Out, " Ownership TXT Record\n") + if ownership.AppValue != "" { + fmt.Fprintf(io.Out, " %s %s \u2192 %s\n", colorize.Cyan("TXT"), ownership.Name, ownership.AppValue) } - } else { - for _, ipnet := range FastlyIPs { - if ipnet.Contains(ip) { - return true - } + if hasCustom && isWildcard { + fmt.Fprintf(io.Out, " %s\n", colorize.Gray("Required to verify ownership for custom wildcard certificates.")) + } else { + fmt.Fprintf(io.Out, " %s\n", colorize.Gray("Required if your app doesn't have an IPv6 address, or if traffic is routed through a CDN or proxy.")) } } - return false -} - -type DNSSetupFlags struct { - Context context.Context - Hostname string - Certificate *fly.AppCertificate - IPv4Address fly.IPAddress - IPv6Address fly.IPAddress - CNAMETarget string - ExternalProxyDetected bool + fmt.Fprintln(io.Out) } -func printDNSSetupOptions(opts DNSSetupFlags) error { - io := iostreams.FromContext(opts.Context) +func printCertificateDetail(ctx context.Context, resp *fly.CertificateDetailResponse) { + io := iostreams.FromContext(ctx) colorize := io.ColorScheme() - hasIPv4 := opts.IPv4Address.Address != "" - hasIPv6 := opts.IPv6Address.Address != "" - promoteExtProxy := opts.ExternalProxyDetected && !opts.Certificate.IsWildcard - fmt.Fprintf(io.Out, "You are creating a certificate for %s\n", colorize.Bold(opts.Hostname)) - fmt.Fprintf(io.Out, "We are using %s for this certificate.\n\n", readableCertAuthority(opts.Certificate.CertificateAuthority)) - - if promoteExtProxy { - fmt.Fprintln(io.Out, colorize.Blue("It looks like your hostname currently resolves to a proxy or CDN.")) - fmt.Fprintln(io.Out, "If you are planning to use a proxy or CDN in front of your Fly application,") - fmt.Fprintf(io.Out, "using the %s will ensure Fly can generate a certificate automatically.\n", colorize.Green("external proxy setup")) - fmt.Fprintln(io.Out) + myprnt := func(label string, value string) { + fmt.Fprintf(io.Out, " %s = %s\n", colorize.Gray(fmt.Sprintf("%-23s", label)), value) } - fmt.Fprintln(io.Out, "You can direct traffic to your Fly application by adding records to your DNS provider.") - fmt.Fprintln(io.Out) - - fmt.Fprintln(io.Out, colorize.Bold("Choose your DNS setup:")) - fmt.Fprintln(io.Out) - - optionNum := 1 - - if promoteExtProxy { - if hasIPv4 { - fmt.Fprintf(io.Out, colorize.Green("%d. External proxy setup\n\n"), optionNum) - fmt.Fprintf(io.Out, " AAAA %s → %s\n\n", getRecordName(opts.Hostname), opts.IPv6Address.Address) - fmt.Fprintln(io.Out, " When proxying traffic, you should only use your application's IPv6 address.") - fmt.Fprintln(io.Out) - optionNum++ + var customCert, flyCert *fly.CertificateDetail + for i := range resp.Certificates { + if resp.Certificates[i].Source == "custom" { + customCert = &resp.Certificates[i] } else { - fmt.Fprintf(io.Out, colorize.Yellow("%d. External proxy setup (requires IPv6 allocation)\n"), optionNum) - fmt.Fprintf(io.Out, " Run: %s to allocate IPv6 address\n", colorize.Bold("fly ips allocate-v6")) - fmt.Fprintf(io.Out, " Then: %s to view these instructions again\n\n", colorize.Bold("fly certs setup "+opts.Hostname)) - fmt.Fprintln(io.Out, " When proxying traffic, you should only use your application's IPv6 address.") - fmt.Fprintln(io.Out) - optionNum++ + flyCert = &resp.Certificates[i] } } - fmt.Fprintf(io.Out, colorize.Green("%d. A and AAAA records (recommended for direct connections)\n\n"), optionNum) - if hasIPv4 { - fmt.Fprintf(io.Out, " A %s → %s\n", getRecordName(opts.Hostname), opts.IPv4Address.Address) - } else { - fmt.Fprintf(io.Out, " %s\n", colorize.Yellow("No IPv4 addresses are allocated for your application.")) - fmt.Fprintf(io.Out, " Run: %s to allocate recommended addresses\n", colorize.Bold("fly ips allocate")) - fmt.Fprintf(io.Out, " Then: %s to view these instructions again\n", colorize.Bold("fly certs setup "+opts.Hostname)) - } - if hasIPv6 { - fmt.Fprintf(io.Out, " AAAA %s → %s\n", getRecordName(opts.Hostname), opts.IPv6Address.Address) - } else { - fmt.Fprintf(io.Out, "\n %s\n", colorize.Yellow("No IPv6 addresses are allocated for your application.")) - fmt.Fprintf(io.Out, " Run: %s to allocate a dedicated IPv6 address\n", colorize.Bold("fly ips allocate-v6")) - fmt.Fprintf(io.Out, " Then: %s to view these instructions again\n", colorize.Bold("fly certs setup "+opts.Hostname)) - } - fmt.Fprintln(io.Out) - optionNum++ - - if !opts.Certificate.IsApex && (hasIPv4 || hasIPv6) && opts.CNAMETarget != "" { - fmt.Fprintf(io.Out, colorize.Cyan("%d. CNAME record\n\n"), optionNum) - fmt.Fprintf(io.Out, " CNAME %s → %s\n", getRecordName(opts.Hostname), opts.CNAMETarget) - fmt.Fprintln(io.Out) - optionNum++ - } - - if !promoteExtProxy && !opts.Certificate.IsWildcard { - fmt.Fprintf(io.Out, colorize.Blue("%d. External proxy setup\n\n"), optionNum) - if hasIPv6 { - fmt.Fprintf(io.Out, " AAAA %s → %s\n\n", getRecordName(opts.Hostname), opts.IPv6Address.Address) + if customCert == nil { + if flyCert != nil { + printCertSection(colorize, myprnt, resp.Hostname, flyCert, "") } else { - fmt.Fprintf(io.Out, " %s\n", colorize.Yellow("No IPv6 addresses are allocated for your application.")) - fmt.Fprintf(io.Out, " Run: %s to allocate a dedicated IPv6 address\n", colorize.Bold("fly ips allocate-v6")) - fmt.Fprintf(io.Out, " Then: %s to view these instructions again\n\n", colorize.Bold("fly certs setup "+opts.Hostname)) + myprnt("Status", colorize.Yellow("Not verified")) + myprnt("Hostname", resp.Hostname) } - fmt.Fprintln(io.Out, " Use this setup when configuring a proxy or CDN in front of your Fly application.") - fmt.Fprintln(io.Out, " When proxying traffic, you should only use your application's IPv6 address.") - fmt.Fprintln(io.Out) - // optionNum++ uncomment if steps added. + return } - if opts.Certificate.IsWildcard { - fmt.Fprint(io.Out, colorize.Yellow("Required: DNS Challenge\n\n")) - } else { - fmt.Fprint(io.Out, colorize.Yellow("Optional: DNS Challenge\n\n")) - } - fmt.Fprintf(io.Out, " CNAME %s → %s\n\n", opts.Certificate.DNSValidationHostname, opts.Certificate.DNSValidationTarget) - fmt.Fprintln(io.Out, " Additional to one of the DNS setups.") - if opts.Certificate.IsWildcard { - fmt.Fprintf(io.Out, " %s\n", colorize.Yellow("Required for this wildcard certificate.")) - } else { - fmt.Fprintln(io.Out, " Required for wildcard certificates, or to generate") - fmt.Fprintln(io.Out, " a certificate before directing traffic to your application.") - } + fmt.Fprintf(io.Out, "%s\n", colorize.Bold("Custom Certificate")) + printCertSection(colorize, myprnt, resp.Hostname, customCert, "") fmt.Fprintln(io.Out) - return nil + fmt.Fprintf(io.Out, "%s\n", colorize.Bold("Fly-Managed Certificate")) + if flyCert != nil { + flyStatus := "" + if customCert.Status == "active" { + flyStatus = "Fallback" + } + printCertSection(colorize, myprnt, resp.Hostname, flyCert, flyStatus) + } else { + myprnt("Status", colorize.Gray("disabled")) + } } -func getRecordName(hostname string) string { - eTLD, _ := publicsuffix.EffectiveTLDPlusOne(hostname) - subdomainname := strings.TrimSuffix(hostname, eTLD) - - if subdomainname == "" { - return "@" +func friendlyStatus(source, rawStatus string) string { + switch rawStatus { + case "active": + if source == "custom" { + return "Verified" + } + return "Issued" + case "pending_ownership": + return "Not verified" + default: + if source == "custom" { + return "Not verified" + } + return "Issuing..." } - return strings.TrimSuffix(subdomainname, ".") } -func printCertificate(ctx context.Context, cert *fly.AppCertificate) { - io := iostreams.FromContext(ctx) +func printCertSection(colorize *iostreams.ColorScheme, myprnt func(string, string), hostname string, cert *fly.CertificateDetail, statusOverride string) { + status := friendlyStatus(cert.Source, cert.Status) + if statusOverride != "" { + status = statusOverride + } - if config.FromContext(ctx).JSONOutput { - render.JSON(io.Out, cert) - return + var coloredStatus string + switch status { + case "Verified", "Issued": + coloredStatus = colorize.Green(status) + case "Issuing...": + coloredStatus = colorize.Yellow(status) + case "Not verified": + coloredStatus = colorize.Yellow(status) + default: + coloredStatus = colorize.Gray(status) } + myprnt("Status", coloredStatus) + myprnt("Hostname", hostname) - myprnt := func(label string, value string) { - fmt.Fprintf(io.Out, "%-25s = %s\n", label, value) + for _, issued := range cert.Issued { + if issued.CertificateAuthority != "" { + myprnt("Certificate Authority", readableCertAuthority(issued.CertificateAuthority)) + break + } } - certtypes := []string{} - var expiresAt string + if cert.Issuer != "" { + myprnt("Issuer", cert.Issuer) + } - for _, v := range cert.Issued.Nodes { - certtypes = append(certtypes, v.Type) - // Get the expiration time (all certs should expire at the same time) - if expiresAt == "" && !v.ExpiresAt.IsZero() { - expiresAt = humanize.Time(v.ExpiresAt) + if len(cert.Issued) > 0 { + var certTypes []string + for _, issued := range cert.Issued { + certTypes = append(certTypes, issued.Type) } + myprnt("Issued", strings.Join(certTypes, ",")) + } + + if cert.CreatedAt != nil && !cert.CreatedAt.IsZero() { + myprnt("Added to App", humanize.Time(*cert.CreatedAt)) } - myprnt("Status", cert.ClientStatus) - myprnt("Hostname", cert.Hostname) - myprnt("DNS Provider", cert.DNSProvider) - myprnt("Certificate Authority", readableCertAuthority(cert.CertificateAuthority)) - myprnt("Issued", strings.Join(certtypes, ",")) - myprnt("Added to App", humanize.Time(cert.CreatedAt)) - if expiresAt != "" { - myprnt("Expires", expiresAt) + if cert.ExpiresAt != nil && !cert.ExpiresAt.IsZero() { + myprnt("Expires", humanize.Time(*cert.ExpiresAt)) + } else if len(cert.Issued) > 0 && !cert.Issued[0].ExpiresAt.IsZero() { + myprnt("Expires", humanize.Time(cert.Issued[0].ExpiresAt)) } - myprnt("Source", cert.Source) } func readableCertAuthority(ca string) string { @@ -564,7 +734,7 @@ func readableCertAuthority(ca string) string { return ca } -func printCertificates(ctx context.Context, certs []fly.AppCertificateCompact) error { +func printCertificates(ctx context.Context, certs []fly.CertificateSummary) error { io := iostreams.FromContext(ctx) if config.FromContext(ctx).JSONOutput { @@ -573,12 +743,38 @@ func printCertificates(ctx context.Context, certs []fly.AppCertificateCompact) e } colorize := io.ColorScheme() - fmt.Fprintf(io.Out, "%-25s %-20s %s\n", "Host Name", "Added", "Status") + fmt.Fprintf(io.Out, "%s\n", colorize.Bold(fmt.Sprintf("%-30s %-10s %s", "HOSTNAME", "SOURCE", "STATUS"))) + for _, v := range certs { - line := fmt.Sprintf("%-25s %-20s %s", v.Hostname, humanize.Time(v.CreatedAt), v.ClientStatus) - if v.ClientStatus == "Ready" { - line = colorize.Green(line) + source := "-" + if v.HasCustomCertificate { + source = "Custom" + } else if v.HasFlyCertificate || v.AcmeRequested { + source = "Fly" + } + + var status string + if v.HasCustomCertificate { + if v.Configured { + status = "Verified" + } else { + status = "Not verified" + } + } else if v.Configured { + status = "Issued" + } else if v.AcmeDNSConfigured || v.AcmeALPNConfigured || v.AcmeHTTPConfigured { + status = "Issuing..." } else { + status = "Not verified" + } + + line := fmt.Sprintf("%-30s %-10s %s", v.Hostname, source, status) + switch status { + case "Verified", "Issued": + line = colorize.Green(line) + case "Not verified": + line = colorize.Yellow(line) + default: line = colorize.Yellow(line) } fmt.Fprintf(io.Out, "%s\n", line) @@ -598,62 +794,3 @@ func getAlternateHostname(hostname string) string { return "www." + hostname } } - -func mustParseCIDR(s string) *net.IPNet { - _, ipnet, err := net.ParseCIDR(s) - if err != nil { - panic(err) - } - return ipnet -} - -const CLOUDFLARE = "cloudflare" - -var CloudflareIPs = []*net.IPNet{ - mustParseCIDR("173.245.48.0/20"), - mustParseCIDR("103.21.244.0/22"), - mustParseCIDR("103.22.200.0/22"), - mustParseCIDR("103.31.4.0/22"), - mustParseCIDR("141.101.64.0/18"), - mustParseCIDR("108.162.192.0/18"), - mustParseCIDR("190.93.240.0/20"), - mustParseCIDR("188.114.96.0/20"), - mustParseCIDR("197.234.240.0/22"), - mustParseCIDR("198.41.128.0/17"), - mustParseCIDR("162.158.0.0/15"), - mustParseCIDR("104.16.0.0/13"), - mustParseCIDR("104.24.0.0/14"), - mustParseCIDR("172.64.0.0/13"), - mustParseCIDR("131.0.72.0/22"), - mustParseCIDR("2400:cb00::/32"), - mustParseCIDR("2606:4700::/32"), - mustParseCIDR("2803:f800::/32"), - mustParseCIDR("2405:b500::/32"), - mustParseCIDR("2405:8100::/32"), - mustParseCIDR("2a06:98c0::/29"), - mustParseCIDR("2c0f:f248::/32"), -} - -var FastlyIPs = []*net.IPNet{ - mustParseCIDR("23.235.32.0/20"), - mustParseCIDR("43.249.72.0/22"), - mustParseCIDR("103.244.50.0/24"), - mustParseCIDR("103.245.222.0/23"), - mustParseCIDR("103.245.224.0/24"), - mustParseCIDR("104.156.80.0/20"), - mustParseCIDR("140.248.64.0/18"), - mustParseCIDR("140.248.128.0/17"), - mustParseCIDR("146.75.0.0/17"), - mustParseCIDR("151.101.0.0/16"), - mustParseCIDR("157.52.64.0/18"), - mustParseCIDR("167.82.0.0/17"), - mustParseCIDR("167.82.128.0/20"), - mustParseCIDR("167.82.160.0/20"), - mustParseCIDR("167.82.224.0/20"), - mustParseCIDR("172.111.64.0/18"), - mustParseCIDR("185.31.16.0/22"), - mustParseCIDR("199.27.72.0/21"), - mustParseCIDR("199.232.0.0/16"), - mustParseCIDR("2a04:4e40::/32"), - mustParseCIDR("2a04:4e42::/32"), -} diff --git a/internal/command/deploy/mock_client_test.go b/internal/command/deploy/mock_client_test.go index 8ed2cfb890..a70968a87a 100644 --- a/internal/command/deploy/mock_client_test.go +++ b/internal/command/deploy/mock_client_test.go @@ -43,14 +43,26 @@ func (m *mockFlapsClient) AssignIP(ctx context.Context, appName string, req flap return nil, fmt.Errorf("failed to assign IP to %s", appName) } +func (m *mockFlapsClient) CheckCertificate(ctx context.Context, appName, hostname string) (*fly.CertificateDetailResponse, error) { + return nil, fmt.Errorf("not implemented") +} + func (m *mockFlapsClient) Cordon(ctx context.Context, appName, machineID string, nonce string) (err error) { return fmt.Errorf("failed to cordon %s", machineID) } +func (m *mockFlapsClient) CreateACMECertificate(ctx context.Context, appName string, req fly.CreateCertificateRequest) (*fly.CertificateDetailResponse, error) { + return nil, fmt.Errorf("not implemented") +} + func (m *mockFlapsClient) CreateApp(ctx context.Context, req flaps.CreateAppRequest) (*flaps.App, error) { return nil, fmt.Errorf("failed to create app") } +func (m *mockFlapsClient) CreateCustomCertificate(ctx context.Context, appName string, req fly.ImportCertificateRequest) (*fly.CertificateDetailResponse, error) { + return nil, fmt.Errorf("not implemented") +} + func (m *mockFlapsClient) CreateVolume(ctx context.Context, appName string, req fly.CreateVolumeRequest) (*fly.Volume, error) { return nil, fmt.Errorf("failed to create volume %s", req.Name) } @@ -59,10 +71,22 @@ func (m *mockFlapsClient) CreateVolumeSnapshot(ctx context.Context, appName, vol return fmt.Errorf("failed to create volume snapshot %s", volumeId) } +func (m *mockFlapsClient) DeleteACMECertificate(ctx context.Context, appName, hostname string) error { + return fmt.Errorf("not implemented") +} + func (m *mockFlapsClient) DeleteApp(ctx context.Context, name string) error { return fmt.Errorf("failed to delete app %s", name) } +func (m *mockFlapsClient) DeleteCertificate(ctx context.Context, appName, hostname string) error { + return fmt.Errorf("not implemented") +} + +func (m *mockFlapsClient) DeleteCustomCertificate(ctx context.Context, appName, hostname string) error { + return fmt.Errorf("not implemented") +} + func (m *mockFlapsClient) DeleteMetadata(ctx context.Context, appName, machineID, key string) error { return fmt.Errorf("failed to delete metadata %s", key) } @@ -119,6 +143,10 @@ func (m *mockFlapsClient) GetAllVolumes(ctx context.Context, appName string) ([] return nil, fmt.Errorf("failed to get all volumes") } +func (m *mockFlapsClient) GetCertificate(ctx context.Context, appName, hostname string) (*fly.CertificateDetailResponse, error) { + return nil, fmt.Errorf("not implemented") +} + func (m *mockFlapsClient) GetIPAssignments(ctx context.Context, appName string) (res *flaps.ListIPAssignmentsResponse, err error) { return nil, fmt.Errorf("failed to get IP assignments for %s", appName) } @@ -188,6 +216,10 @@ func (m *mockFlapsClient) ListApps(ctx context.Context, req flaps.ListAppsReques return nil, fmt.Errorf("failed to list apps") } +func (m *mockFlapsClient) ListCertificates(ctx context.Context, appName string, opts *flaps.ListCertificatesOpts) (*fly.ListCertificatesResponse, error) { + return nil, fmt.Errorf("not implemented") +} + func (m *mockFlapsClient) ListFlyAppsMachines(ctx context.Context, appName string) ([]*fly.Machine, *fly.Machine, error) { return nil, nil, fmt.Errorf("failed to list fly apps machines") } diff --git a/internal/flapsutil/flaps_client.go b/internal/flapsutil/flaps_client.go index fba0f3ddb6..cd9f14e2f6 100644 --- a/internal/flapsutil/flaps_client.go +++ b/internal/flapsutil/flaps_client.go @@ -14,11 +14,16 @@ var _ FlapsClient = (*flaps.Client)(nil) type FlapsClient interface { AcquireLease(ctx context.Context, appName, machineID string, ttl *int) (*fly.MachineLease, error) AssignIP(ctx context.Context, appName string, req flaps.AssignIPRequest) (res *flaps.IPAssignment, err error) + CheckCertificate(ctx context.Context, appName, hostname string) (*fly.CertificateDetailResponse, error) Cordon(ctx context.Context, appName, machineID string, nonce string) (err error) CreateApp(ctx context.Context, req flaps.CreateAppRequest) (*flaps.App, error) + CreateACMECertificate(ctx context.Context, appName string, req fly.CreateCertificateRequest) (*fly.CertificateDetailResponse, error) CreateVolume(ctx context.Context, appName string, req fly.CreateVolumeRequest) (*fly.Volume, error) CreateVolumeSnapshot(ctx context.Context, appName, volumeId string) error DeleteApp(ctx context.Context, name string) error + DeleteACMECertificate(ctx context.Context, appName, hostname string) error + DeleteCertificate(ctx context.Context, appName, hostname string) error + DeleteCustomCertificate(ctx context.Context, appName, hostname string) error DeleteMetadata(ctx context.Context, appName, machineID, key string) error DeleteAppSecret(ctx context.Context, appName, name string) (*fly.DeleteAppSecretResp, error) DeleteIPAssignment(ctx context.Context, appName, ip string) (err error) @@ -32,6 +37,7 @@ type FlapsClient interface { Get(ctx context.Context, appName, machineID string) (*fly.Machine, error) GetApp(ctx context.Context, name string) (app *flaps.App, err error) GetAllVolumes(ctx context.Context, appName string) ([]fly.Volume, error) + GetCertificate(ctx context.Context, appName, hostname string) (*fly.CertificateDetailResponse, error) GetIPAssignments(ctx context.Context, appName string) (res *flaps.ListIPAssignmentsResponse, err error) GetMany(ctx context.Context, appName string, machineIDs []string) ([]*fly.Machine, error) GetMetadata(ctx context.Context, appName, machineID string) (map[string]string, error) @@ -41,13 +47,15 @@ type FlapsClient interface { GetVolume(ctx context.Context, appName, volumeId string) (*fly.Volume, error) GetVolumeSnapshots(ctx context.Context, appName, volumeId string) ([]fly.VolumeSnapshot, error) GetVolumes(ctx context.Context, appName string) ([]fly.Volume, error) + CreateCustomCertificate(ctx context.Context, appName string, req fly.ImportCertificateRequest) (*fly.CertificateDetailResponse, error) Kill(ctx context.Context, appName, machineID string) (err error) Launch(ctx context.Context, appName string, builder fly.LaunchMachineInput) (out *fly.Machine, err error) List(ctx context.Context, appName, state string) ([]*fly.Machine, error) ListActive(ctx context.Context, appName string) ([]*fly.Machine, error) ListApps(ctx context.Context, req flaps.ListAppsRequest) ([]flaps.App, error) - ListFlyAppsMachines(ctx context.Context, appName string) ([]*fly.Machine, *fly.Machine, error) ListAppSecrets(ctx context.Context, appName string, version *uint64, showSecrets bool) ([]fly.AppSecret, error) + ListCertificates(ctx context.Context, appName string, opts *flaps.ListCertificatesOpts) (*fly.ListCertificatesResponse, error) + ListFlyAppsMachines(ctx context.Context, appName string) ([]*fly.Machine, *fly.Machine, error) ListSecretKeys(ctx context.Context, appName string, version *uint64) ([]fly.SecretKey, error) NewRequest(ctx context.Context, method, path string, in interface{}, headers map[string][]string) (*http.Request, error) RefreshLease(ctx context.Context, appName, machineID string, ttl *int, nonce string) (*fly.MachineLease, error) diff --git a/internal/inmem/flaps_client.go b/internal/inmem/flaps_client.go index 600836260f..6b461f9f17 100644 --- a/internal/inmem/flaps_client.go +++ b/internal/inmem/flaps_client.go @@ -33,6 +33,10 @@ func (m *FlapsClient) AssignIP(ctx context.Context, appName string, req flaps.As panic("TODO") } +func (m *FlapsClient) CheckCertificate(ctx context.Context, appName, hostname string) (*fly.CertificateDetailResponse, error) { + panic("TODO") +} + func (m *FlapsClient) Cordon(ctx context.Context, appName, machineID string, nonce string) (err error) { panic("TODO") } @@ -41,6 +45,10 @@ func (m *FlapsClient) CreateApp(ctx context.Context, req flaps.CreateAppRequest) panic("TODO") } +func (m *FlapsClient) CreateACMECertificate(ctx context.Context, appName string, req fly.CreateCertificateRequest) (*fly.CertificateDetailResponse, error) { + panic("TODO") +} + func (m *FlapsClient) CreateVolume(ctx context.Context, appName string, req fly.CreateVolumeRequest) (*fly.Volume, error) { panic("TODO") } @@ -53,6 +61,18 @@ func (m *FlapsClient) DeleteApp(ctx context.Context, name string) error { panic("TODO") } +func (m *FlapsClient) DeleteACMECertificate(ctx context.Context, appName, hostname string) error { + panic("TODO") +} + +func (m *FlapsClient) DeleteCertificate(ctx context.Context, appName, hostname string) error { + panic("TODO") +} + +func (m *FlapsClient) DeleteCustomCertificate(ctx context.Context, appName, hostname string) error { + panic("TODO") +} + func (m *FlapsClient) DeleteMetadata(ctx context.Context, appName, machineID, key string) error { panic("TODO") } @@ -118,6 +138,10 @@ func (m *FlapsClient) GetAllVolumes(ctx context.Context, appName string) ([]fly. panic("TODO") } +func (m *FlapsClient) GetCertificate(ctx context.Context, appName, hostname string) (*fly.CertificateDetailResponse, error) { + panic("TODO") +} + func (m *FlapsClient) GetIPAssignments(ctx context.Context, appName string) (res *flaps.ListIPAssignmentsResponse, err error) { return &flaps.ListIPAssignmentsResponse{}, nil } @@ -154,6 +178,10 @@ func (m *FlapsClient) GetVolumes(ctx context.Context, appName string) ([]fly.Vol panic("TODO") } +func (m *FlapsClient) CreateCustomCertificate(ctx context.Context, appName string, req fly.ImportCertificateRequest) (*fly.CertificateDetailResponse, error) { + panic("TODO") +} + func (m *FlapsClient) Kill(ctx context.Context, appName, machineID string) (err error) { panic("TODO") } @@ -183,6 +211,14 @@ func (m *FlapsClient) ListApps(ctx context.Context, req flaps.ListAppsRequest) ( panic("TODO") } +func (m *FlapsClient) ListAppSecrets(ctx context.Context, appName string, version *uint64, showSecrets bool) ([]fly.AppSecret, error) { + panic("TODO") +} + +func (m *FlapsClient) ListCertificates(ctx context.Context, appName string, opts *flaps.ListCertificatesOpts) (*fly.ListCertificatesResponse, error) { + panic("TODO") +} + func (m *FlapsClient) ListFlyAppsMachines(ctx context.Context, appName string) (machines []*fly.Machine, releaseCmdMachine *fly.Machine, err error) { m.server.mu.Lock() defer m.server.mu.Unlock() @@ -198,10 +234,6 @@ func (m *FlapsClient) ListFlyAppsMachines(ctx context.Context, appName string) ( return machines, releaseCmdMachine, nil } -func (m *FlapsClient) ListAppSecrets(ctx context.Context, appName string, version *uint64, showSecrets bool) ([]fly.AppSecret, error) { - panic("TODO") -} - func (m *FlapsClient) ListSecretKeys(ctx context.Context, appName string, version *uint64) ([]fly.SecretKey, error) { panic("TODO") } diff --git a/internal/mock/flaps_client.go b/internal/mock/flaps_client.go index 168b93018f..f2de85ad98 100644 --- a/internal/mock/flaps_client.go +++ b/internal/mock/flaps_client.go @@ -13,59 +13,67 @@ import ( var _ flapsutil.FlapsClient = (*FlapsClient)(nil) type FlapsClient struct { - AcquireLeaseFunc func(ctx context.Context, appName, machineID string, ttl *int) (*fly.MachineLease, error) - AssignIPFunc func(ctx context.Context, appName string, req flaps.AssignIPRequest) (res *flaps.IPAssignment, err error) - CordonFunc func(ctx context.Context, appName, machineID string, nonce string) (err error) - CreateAppFunc func(ctx context.Context, req flaps.CreateAppRequest) (*flaps.App, error) - CreateVolumeFunc func(ctx context.Context, appName string, req fly.CreateVolumeRequest) (*fly.Volume, error) - CreateVolumeSnapshotFunc func(ctx context.Context, appName, volumeId string) error - DeleteAppFunc func(ctx context.Context, name string) error - DeleteMetadataFunc func(ctx context.Context, appName, machineID, key string) error - DeleteAppSecretFunc func(ctx context.Context, appName, name string) (*fly.DeleteAppSecretResp, error) - DeleteIPAssignmentFunc func(ctx context.Context, appName, ip string) (err error) - DeleteSecretKeyFunc func(ctx context.Context, appName, name string) error - DeleteVolumeFunc func(ctx context.Context, appName, volumeId string) (*fly.Volume, error) - DestroyFunc func(ctx context.Context, appName string, input fly.RemoveMachineInput, nonce string) (err error) - ExecFunc func(ctx context.Context, appName, machineID string, in *fly.MachineExecRequest) (*fly.MachineExecResponse, error) - ExtendVolumeFunc func(ctx context.Context, appName, volumeId string, size_gb int) (*fly.Volume, bool, error) - FindLeaseFunc func(ctx context.Context, appName, machineID string) (*fly.MachineLease, error) - GenerateSecretKeyFunc func(ctx context.Context, appName, name string, typ string) (*fly.SetSecretKeyResp, error) - GetFunc func(ctx context.Context, appName, machineID string) (*fly.Machine, error) - GetAppFunc func(ctx context.Context, name string) (app *flaps.App, err error) - GetAllVolumesFunc func(ctx context.Context, appName string) ([]fly.Volume, error) - GetIPAssignmentsFunc func(ctx context.Context, appName string) (res *flaps.ListIPAssignmentsResponse, err error) - GetManyFunc func(ctx context.Context, appName string, machineIDs []string) ([]*fly.Machine, error) - GetMetadataFunc func(ctx context.Context, appName, machineID string) (map[string]string, error) - GetPlacementsFunc func(ctx context.Context, req *flaps.GetPlacementsRequest) ([]flaps.RegionPlacement, error) - GetProcessesFunc func(ctx context.Context, appName, machineID string) (fly.MachinePsResponse, error) - GetRegionsFunc func(ctx context.Context) (*flaps.RegionData, error) - GetVolumeFunc func(ctx context.Context, appName, volumeId string) (*fly.Volume, error) - GetVolumeSnapshotsFunc func(ctx context.Context, appName, volumeId string) ([]fly.VolumeSnapshot, error) - GetVolumesFunc func(ctx context.Context, appName string) ([]fly.Volume, error) - KillFunc func(ctx context.Context, appName, machineID string) (err error) - LaunchFunc func(ctx context.Context, appName string, builder fly.LaunchMachineInput) (out *fly.Machine, err error) - ListFunc func(ctx context.Context, appName, state string) ([]*fly.Machine, error) - ListActiveFunc func(ctx context.Context, appName string) ([]*fly.Machine, error) - ListAppsFunc func(ctx context.Context, req flaps.ListAppsRequest) ([]flaps.App, error) - ListFlyAppsMachinesFunc func(ctx context.Context, appName string) ([]*fly.Machine, *fly.Machine, error) - ListAppSecretsFunc func(ctx context.Context, appName string, version *uint64, showSecrets bool) ([]fly.AppSecret, error) - ListSecretKeysFunc func(ctx context.Context, appName string, version *uint64) ([]fly.SecretKey, error) - NewRequestFunc func(ctx context.Context, method, path string, in interface{}, headers map[string][]string) (*http.Request, error) - RefreshLeaseFunc func(ctx context.Context, appName, machineID string, ttl *int, nonce string) (*fly.MachineLease, error) - ReleaseLeaseFunc func(ctx context.Context, appName, machineID, nonce string) error - RestartFunc func(ctx context.Context, appName string, in fly.RestartMachineInput, nonce string) (err error) - SetMetadataFunc func(ctx context.Context, appName, machineID, key, value string) error - SetAppSecretFunc func(ctx context.Context, appName, name string, value string) (*fly.SetAppSecretResp, error) - SetSecretKeyFunc func(ctx context.Context, appName, name string, typ string, value []byte) (*fly.SetSecretKeyResp, error) - StartFunc func(ctx context.Context, appName, machineID string, nonce string) (out *fly.MachineStartResponse, err error) - StopFunc func(ctx context.Context, appName string, in fly.StopMachineInput, nonce string) (err error) - SuspendFunc func(ctx context.Context, appName, machineID, nonce string) error - UncordonFunc func(ctx context.Context, appName, machineID string, nonce string) (err error) - UpdateFunc func(ctx context.Context, appName string, builder fly.LaunchMachineInput, nonce string) (out *fly.Machine, err error) - UpdateAppSecretsFunc func(ctx context.Context, appName string, values map[string]*string) (*fly.UpdateAppSecretsResp, error) - UpdateVolumeFunc func(ctx context.Context, appName, volumeId string, req fly.UpdateVolumeRequest) (*fly.Volume, error) - WaitFunc func(ctx context.Context, appName string, machine *fly.Machine, state string, timeout time.Duration) (err error) - WaitForAppFunc func(ctx context.Context, name string) error + AcquireLeaseFunc func(ctx context.Context, appName, machineID string, ttl *int) (*fly.MachineLease, error) + AssignIPFunc func(ctx context.Context, appName string, req flaps.AssignIPRequest) (res *flaps.IPAssignment, err error) + CheckCertificateFunc func(ctx context.Context, appName, hostname string) (*fly.CertificateDetailResponse, error) + CordonFunc func(ctx context.Context, appName, machineID string, nonce string) (err error) + CreateAppFunc func(ctx context.Context, req flaps.CreateAppRequest) (*flaps.App, error) + CreateACMECertificateFunc func(ctx context.Context, appName string, req fly.CreateCertificateRequest) (*fly.CertificateDetailResponse, error) + CreateVolumeFunc func(ctx context.Context, appName string, req fly.CreateVolumeRequest) (*fly.Volume, error) + CreateVolumeSnapshotFunc func(ctx context.Context, appName, volumeId string) error + DeleteAppFunc func(ctx context.Context, name string) error + DeleteACMECertificateFunc func(ctx context.Context, appName, hostname string) error + DeleteCertificateFunc func(ctx context.Context, appName, hostname string) error + DeleteCustomCertificateFunc func(ctx context.Context, appName, hostname string) error + DeleteMetadataFunc func(ctx context.Context, appName, machineID, key string) error + DeleteAppSecretFunc func(ctx context.Context, appName, name string) (*fly.DeleteAppSecretResp, error) + DeleteIPAssignmentFunc func(ctx context.Context, appName, ip string) (err error) + DeleteSecretKeyFunc func(ctx context.Context, appName, name string) error + DeleteVolumeFunc func(ctx context.Context, appName, volumeId string) (*fly.Volume, error) + DestroyFunc func(ctx context.Context, appName string, input fly.RemoveMachineInput, nonce string) (err error) + ExecFunc func(ctx context.Context, appName, machineID string, in *fly.MachineExecRequest) (*fly.MachineExecResponse, error) + ExtendVolumeFunc func(ctx context.Context, appName, volumeId string, size_gb int) (*fly.Volume, bool, error) + FindLeaseFunc func(ctx context.Context, appName, machineID string) (*fly.MachineLease, error) + GenerateSecretKeyFunc func(ctx context.Context, appName, name string, typ string) (*fly.SetSecretKeyResp, error) + GetFunc func(ctx context.Context, appName, machineID string) (*fly.Machine, error) + GetAppFunc func(ctx context.Context, name string) (app *flaps.App, err error) + GetAllVolumesFunc func(ctx context.Context, appName string) ([]fly.Volume, error) + GetCertificateFunc func(ctx context.Context, appName, hostname string) (*fly.CertificateDetailResponse, error) + GetIPAssignmentsFunc func(ctx context.Context, appName string) (res *flaps.ListIPAssignmentsResponse, err error) + GetManyFunc func(ctx context.Context, appName string, machineIDs []string) ([]*fly.Machine, error) + GetMetadataFunc func(ctx context.Context, appName, machineID string) (map[string]string, error) + GetPlacementsFunc func(ctx context.Context, req *flaps.GetPlacementsRequest) ([]flaps.RegionPlacement, error) + GetProcessesFunc func(ctx context.Context, appName, machineID string) (fly.MachinePsResponse, error) + GetRegionsFunc func(ctx context.Context) (*flaps.RegionData, error) + GetVolumeFunc func(ctx context.Context, appName, volumeId string) (*fly.Volume, error) + GetVolumeSnapshotsFunc func(ctx context.Context, appName, volumeId string) ([]fly.VolumeSnapshot, error) + GetVolumesFunc func(ctx context.Context, appName string) ([]fly.Volume, error) + CreateCustomCertificateFunc func(ctx context.Context, appName string, req fly.ImportCertificateRequest) (*fly.CertificateDetailResponse, error) + KillFunc func(ctx context.Context, appName, machineID string) (err error) + LaunchFunc func(ctx context.Context, appName string, builder fly.LaunchMachineInput) (out *fly.Machine, err error) + ListFunc func(ctx context.Context, appName, state string) ([]*fly.Machine, error) + ListActiveFunc func(ctx context.Context, appName string) ([]*fly.Machine, error) + ListAppsFunc func(ctx context.Context, req flaps.ListAppsRequest) ([]flaps.App, error) + ListAppSecretsFunc func(ctx context.Context, appName string, version *uint64, showSecrets bool) ([]fly.AppSecret, error) + ListCertificatesFunc func(ctx context.Context, appName string, opts *flaps.ListCertificatesOpts) (*fly.ListCertificatesResponse, error) + ListFlyAppsMachinesFunc func(ctx context.Context, appName string) ([]*fly.Machine, *fly.Machine, error) + ListSecretKeysFunc func(ctx context.Context, appName string, version *uint64) ([]fly.SecretKey, error) + NewRequestFunc func(ctx context.Context, method, path string, in interface{}, headers map[string][]string) (*http.Request, error) + RefreshLeaseFunc func(ctx context.Context, appName, machineID string, ttl *int, nonce string) (*fly.MachineLease, error) + ReleaseLeaseFunc func(ctx context.Context, appName, machineID, nonce string) error + RestartFunc func(ctx context.Context, appName string, in fly.RestartMachineInput, nonce string) (err error) + SetMetadataFunc func(ctx context.Context, appName, machineID, key, value string) error + SetAppSecretFunc func(ctx context.Context, appName, name string, value string) (*fly.SetAppSecretResp, error) + SetSecretKeyFunc func(ctx context.Context, appName, name string, typ string, value []byte) (*fly.SetSecretKeyResp, error) + StartFunc func(ctx context.Context, appName, machineID string, nonce string) (out *fly.MachineStartResponse, err error) + StopFunc func(ctx context.Context, appName string, in fly.StopMachineInput, nonce string) (err error) + SuspendFunc func(ctx context.Context, appName, machineID, nonce string) error + UncordonFunc func(ctx context.Context, appName, machineID string, nonce string) (err error) + UpdateFunc func(ctx context.Context, appName string, builder fly.LaunchMachineInput, nonce string) (out *fly.Machine, err error) + UpdateAppSecretsFunc func(ctx context.Context, appName string, values map[string]*string) (*fly.UpdateAppSecretsResp, error) + UpdateVolumeFunc func(ctx context.Context, appName, volumeId string, req fly.UpdateVolumeRequest) (*fly.Volume, error) + WaitFunc func(ctx context.Context, appName string, machine *fly.Machine, state string, timeout time.Duration) (err error) + WaitForAppFunc func(ctx context.Context, name string) error } func (m *FlapsClient) AcquireLease(ctx context.Context, appName, machineID string, ttl *int) (*fly.MachineLease, error) { @@ -76,6 +84,10 @@ func (m *FlapsClient) AssignIP(ctx context.Context, appName string, req flaps.As return m.AssignIPFunc(ctx, appName, req) } +func (m *FlapsClient) CheckCertificate(ctx context.Context, appName, hostname string) (*fly.CertificateDetailResponse, error) { + return m.CheckCertificateFunc(ctx, appName, hostname) +} + func (m *FlapsClient) Cordon(ctx context.Context, appName, machineID string, nonce string) (err error) { return m.CordonFunc(ctx, appName, machineID, nonce) } @@ -84,6 +96,10 @@ func (m *FlapsClient) CreateApp(ctx context.Context, req flaps.CreateAppRequest) return m.CreateAppFunc(ctx, req) } +func (m *FlapsClient) CreateACMECertificate(ctx context.Context, appName string, req fly.CreateCertificateRequest) (*fly.CertificateDetailResponse, error) { + return m.CreateACMECertificateFunc(ctx, appName, req) +} + func (m *FlapsClient) CreateVolume(ctx context.Context, appName string, req fly.CreateVolumeRequest) (*fly.Volume, error) { return m.CreateVolumeFunc(ctx, appName, req) } @@ -96,6 +112,18 @@ func (m *FlapsClient) DeleteApp(ctx context.Context, name string) error { return m.DeleteAppFunc(ctx, name) } +func (m *FlapsClient) DeleteACMECertificate(ctx context.Context, appName, hostname string) error { + return m.DeleteACMECertificateFunc(ctx, appName, hostname) +} + +func (m *FlapsClient) DeleteCertificate(ctx context.Context, appName, hostname string) error { + return m.DeleteCertificateFunc(ctx, appName, hostname) +} + +func (m *FlapsClient) DeleteCustomCertificate(ctx context.Context, appName, hostname string) error { + return m.DeleteCustomCertificateFunc(ctx, appName, hostname) +} + func (m *FlapsClient) DeleteMetadata(ctx context.Context, appName, machineID, key string) error { return m.DeleteMetadataFunc(ctx, appName, machineID, key) } @@ -148,6 +176,10 @@ func (m *FlapsClient) GetAllVolumes(ctx context.Context, appName string) ([]fly. return m.GetAllVolumesFunc(ctx, appName) } +func (m *FlapsClient) GetCertificate(ctx context.Context, appName, hostname string) (*fly.CertificateDetailResponse, error) { + return m.GetCertificateFunc(ctx, appName, hostname) +} + func (m *FlapsClient) GetIPAssignments(ctx context.Context, appName string) (res *flaps.ListIPAssignmentsResponse, err error) { return m.GetIPAssignmentsFunc(ctx, appName) } @@ -184,6 +216,10 @@ func (m *FlapsClient) GetVolumes(ctx context.Context, appName string) ([]fly.Vol return m.GetVolumesFunc(ctx, appName) } +func (m *FlapsClient) CreateCustomCertificate(ctx context.Context, appName string, req fly.ImportCertificateRequest) (*fly.CertificateDetailResponse, error) { + return m.CreateCustomCertificateFunc(ctx, appName, req) +} + func (m *FlapsClient) Kill(ctx context.Context, appName, machineID string) (err error) { return m.KillFunc(ctx, appName, machineID) } @@ -204,14 +240,18 @@ func (m *FlapsClient) ListApps(ctx context.Context, req flaps.ListAppsRequest) ( return m.ListAppsFunc(ctx, req) } -func (m *FlapsClient) ListFlyAppsMachines(ctx context.Context, appName string) ([]*fly.Machine, *fly.Machine, error) { - return m.ListFlyAppsMachinesFunc(ctx, appName) -} - func (m *FlapsClient) ListAppSecrets(ctx context.Context, appName string, version *uint64, showSecrets bool) ([]fly.AppSecret, error) { return m.ListAppSecretsFunc(ctx, appName, version, showSecrets) } +func (m *FlapsClient) ListCertificates(ctx context.Context, appName string, opts *flaps.ListCertificatesOpts) (*fly.ListCertificatesResponse, error) { + return m.ListCertificatesFunc(ctx, appName, opts) +} + +func (m *FlapsClient) ListFlyAppsMachines(ctx context.Context, appName string) ([]*fly.Machine, *fly.Machine, error) { + return m.ListFlyAppsMachinesFunc(ctx, appName) +} + func (m *FlapsClient) ListSecretKeys(ctx context.Context, appName string, version *uint64) ([]fly.SecretKey, error) { return m.ListSecretKeysFunc(ctx, appName, version) } From 4f1c1204cb2192da83de013f113c885cdbe3c9f3 Mon Sep 17 00:00:00 2001 From: Liam Bigelow Date: Fri, 13 Feb 2026 00:21:08 +1300 Subject: [PATCH 2/2] Tidy go.sum --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index a204d23f05..c794dcea61 100644 --- a/go.sum +++ b/go.sum @@ -637,8 +637,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/superfly/fly-go v0.3.0 h1:opzCfB5GeDe2AaqZgtPzWps+fW1zAMhISWAyQrhnV+Y= -github.com/superfly/fly-go v0.3.0/go.mod h1:2gCFoNR3iUELADGTJtbBoviMa2jlh2vlPK3cKUajOp8= github.com/superfly/fly-go v0.3.1 h1:XWrxqQOpZhWFP0gpNgKFec1iXxOAwuW7dYWJW+hh8M8= github.com/superfly/fly-go v0.3.1/go.mod h1:2gCFoNR3iUELADGTJtbBoviMa2jlh2vlPK3cKUajOp8= github.com/superfly/graphql v0.2.6 h1:zppbodNerWecoXEdjkhrqaNaSjGqobhXNlViHFuZzb4=