Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion src/pkg/cli/cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ var (
httpRetryDelayBase = 5 * time.Second
)

// CertIssuer is implemented by providers that issue and bind TLS certificates
// directly against their cloud's API rather than going through the
// CNAME→fabric→ACME redirect dance used on AWS BYOD / Playground. Azure
// implements this so `defang cert generate` can drive the Container Apps
// hostname-add + managed-cert + SniEnabled-bind sequence end-to-end.
type CertIssuer interface {
IssueCert(ctx context.Context, projectName, serviceName, hostname string) error
}

func GenerateLetsEncryptCert(ctx context.Context, project *compose.Project, client client.FabricClient, provider client.Provider) error {
term.Debugf("Generating TLS cert for project %q", project.Name)

Expand All @@ -88,6 +97,9 @@ func GenerateLetsEncryptCert(ctx context.Context, project *compose.Project, clie
return fmt.Errorf("no services found for project %q; deployment may not be finished yet", project.Name)
}

issuer, _ := provider.(CertIssuer)

var issueErrs []error
cnt := 0
for _, serviceInfo := range services.Services {
if !serviceInfo.UseAcmeCert {
Expand All @@ -98,11 +110,23 @@ func GenerateLetsEncryptCert(ctx context.Context, project *compose.Project, clie
term.Warnf("service %q: domainname %q in compose file does not match deployed value %q", service.Name, service.DomainName, serviceInfo.Domainname)
}
cnt++
targets := getDomainTargets(serviceInfo, service)
domains := []string{service.DomainName}
if defaultNetwork := service.Networks["default"]; defaultNetwork != nil {
domains = append(domains, defaultNetwork.Aliases...)
}

if issuer != nil {
term.Debugf("Issuing certs for service %v with domains %v via provider", service.Name, domains)
for _, domain := range domains {
if err := issuer.IssueCert(ctx, project.Name, service.Name, domain); err != nil {
term.Errorf("Cert issuance for %v failed: %v", domain, err)
issueErrs = append(issueErrs, fmt.Errorf("%v: %w", domain, err))
}
}
continue
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

targets := getDomainTargets(serviceInfo, service)
term.Debugf("Found service %v with domains %v and targets %v", service.Name, domains, targets)
for _, domain := range domains {
generateCert(ctx, domain, targets, client)
Expand All @@ -113,6 +137,9 @@ func GenerateLetsEncryptCert(ctx context.Context, project *compose.Project, clie
term.Infof("No `domainname` found in compose file; no HTTPS cert generation needed")
}

if len(issueErrs) > 0 {
return fmt.Errorf("certificate issuance failed for one or more domains; verify DNS records and retry `defang cert generate`: %w", errors.Join(issueErrs...))
}
return nil
}

Expand Down
15 changes: 15 additions & 0 deletions src/pkg/cli/client/byoc/azure/byoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/DefangLabs/defang/src/pkg/tokenstore"
"github.com/DefangLabs/defang/src/pkg/types"
defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1"
composeTypes "github.com/compose-spec/compose-go/v2/types"
"golang.org/x/sync/errgroup"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
Expand Down Expand Up @@ -912,3 +913,17 @@ func (b *ByocAzure) TearDownCD(context.Context) error {
func (b *ByocAzure) UpdateShardDomain(context.Context) error {
return fmt.Errorf("UpdateShardDomain: %w", errors.ErrUnsupported)
}

// UpdateServiceInfo implements byoc.ServiceInfoUpdater. When a service has a
// `domainname` set in compose, mark it for managed-cert issuance so
// `defang cert generate` picks it up via the CertIssuer path. Azure Container
// Apps managed certs are free, auto-renewing, and validated via CNAME — no
// hosted-zone presence required (unlike AWS, where ZoneId triggers a different
// path).
func (b *ByocAzure) UpdateServiceInfo(_ context.Context, si *defangv1.ServiceInfo, _, _ string, service composeTypes.ServiceConfig) error {
if service.DomainName == "" {
return nil
}
si.UseAcmeCert = true
return nil
}
Loading
Loading