Skip to content

Commit 633254a

Browse files
committed
modified: internal/service/site_test.go
1 parent dac0b0e commit 633254a

File tree

5 files changed

+164
-8
lines changed

5 files changed

+164
-8
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ ols site list
9393

9494
```bash
9595
ols --dry-run site create example.com --wp --le --php85 --enable-owasp --hsts --enable-ns
96-
ols --dry-run site update example.com --enable-recaptcha --disable-owasp --disable-ns
96+
ols --dry-run site update example.com --enable-recaptcha --disable-owasp --disable-ns --le
9797
ols --dry-run site info example.com
9898
ols --dry-run site show example.com
9999
ols --dry-run site list
@@ -125,7 +125,7 @@ ols site (command) [options]
125125
| Subcommand | Purpose | Options |
126126
| --- | --- | --- |
127127
| `create` | Create a new site/vhost | `--wp` `--le` `--php81` `--php82` `--php83` `--php84` `--php85` `--enable-owasp` `--disable-owasp` `--enable-recaptcha` `--disable-recaptcha` `--enable-ns` `--disable-ns` `--hsts` |
128-
| `update` | Update an existing site (PHP target optional when only security flags are used) | `--wp` (requires one of `--php81` `--php82` `--php83` `--php84` `--php85`), or security flags only: `--enable-owasp` `--disable-owasp` `--enable-recaptcha` `--disable-recaptcha` `--enable-ns` `--disable-ns` `--hsts` |
128+
| `update` | Update an existing site (PHP target optional when only security/LE flags are used) | `--wp` (requires one of `--php81` `--php82` `--php83` `--php84` `--php85`), optional `--le`, or security flags only: `--enable-owasp` `--disable-owasp` `--enable-recaptcha` `--disable-recaptcha` `--enable-ns` `--disable-ns` `--hsts` |
129129
| `info` | Show site metadata and detected status | |
130130
| `show` | Print OLS virtual host config (`vhconf.conf`) | |
131131
| `list` | List managed sites discovered from OLS vhost directory | |

internal/cli/site.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ func newSiteUpdateCmd(svc siteManager, rootOpts *rootOptions) *cobra.Command {
173173
owasp := &toggleFlags{}
174174
recaptcha := &toggleFlags{}
175175
var withWordPress bool
176+
var withLE bool
176177
var withHSTS bool
177178
namespace := &toggleFlags{}
178179

@@ -181,7 +182,8 @@ func newSiteUpdateCmd(svc siteManager, rootOpts *rootOptions) *cobra.Command {
181182
Short: "Update existing site configuration",
182183
Example: "ols site update example.com --php83\n" +
183184
"ols site update example.com --enable-owasp --enable-recaptcha\n" +
184-
"ols --dry-run site update example.com --wp --php85 --hsts",
185+
"ols site update example.com --le\n" +
186+
"ols --dry-run site update example.com --wp --php85 --hsts --le",
185187
Args: cobra.ExactArgs(1),
186188
RunE: func(cmd *cobra.Command, args []string) error {
187189
phpVersion, err := php.selected("")
@@ -205,12 +207,13 @@ func newSiteUpdateCmd(svc siteManager, rootOpts *rootOptions) *cobra.Command {
205207
if withWordPress && phpVersion == "" {
206208
return apperr.New(apperr.CodeValidation, "missing PHP version flag for --wp; provide one of --php81/--php82/--php83/--php84/--php85")
207209
}
208-
if phpVersion == "" && !withWordPress && owaspEnabled == nil && recaptchaEnabled == nil && namespaceEnabled == nil && !withHSTS {
209-
return apperr.New(apperr.CodeValidation, "no update action provided; pass PHP version and/or security flags such as --enable-owasp, --enable-recaptcha, --disable-owasp, --disable-recaptcha, --enable-ns, --disable-ns, --hsts")
210+
if phpVersion == "" && !withWordPress && !withLE && owaspEnabled == nil && recaptchaEnabled == nil && namespaceEnabled == nil && !withHSTS {
211+
return apperr.New(apperr.CodeValidation, "no update action provided; pass PHP version and/or flags such as --wp, --le, --enable-owasp, --enable-recaptcha, --disable-owasp, --disable-recaptcha, --enable-ns, --disable-ns, --hsts")
210212
}
211213
err = svc.UpdateSitePHP(cmd.Context(), service.UpdateSiteOptions{
212214
Domain: args[0],
213215
WithWordPress: withWordPress,
216+
WithLE: withLE,
214217
PHPVersion: phpVersion,
215218
OWASPEnabled: owaspEnabled,
216219
RecaptchaEnabled: recaptchaEnabled,
@@ -227,6 +230,7 @@ func newSiteUpdateCmd(svc siteManager, rootOpts *rootOptions) *cobra.Command {
227230

228231
cmd.Flags().SortFlags = false
229232
cmd.Flags().BoolVar(&withWordPress, "wp", false, "ensure WordPress and LiteSpeed Cache plugin are present")
233+
cmd.Flags().BoolVar(&withLE, "le", false, "issue/update Let's Encrypt certificate for the site")
230234
addPHPVersionFlags(cmd, php)
231235
cmd.Flags().BoolVar(&owasp.enable, "enable-owasp", false, "enable OWASP ModSecurity at virtual host level")
232236
cmd.Flags().BoolVar(&recaptcha.enable, "enable-recaptcha", false, "enable reCAPTCHA at virtual host level")

internal/cli/site_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ func TestSiteUpdateFlagOrder(t *testing.T) {
144144

145145
wantPrefix := []string{
146146
"wp",
147+
"le",
147148
"php81",
148149
"php82",
149150
"php83",

internal/service/site.go

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ type CreateSiteOptions struct {
135135
type UpdateSiteOptions struct {
136136
Domain string
137137
WithWordPress bool
138+
WithLE bool
138139
PHPVersion string
139140
OWASPEnabled *bool
140141
RecaptchaEnabled *bool
@@ -509,8 +510,10 @@ func (s SiteService) UpdateSitePHP(ctx context.Context, opts UpdateSiteOptions)
509510

510511
phpRequested := strings.TrimSpace(opts.PHPVersion) != ""
511512
securityRequested := opts.OWASPEnabled != nil || opts.RecaptchaEnabled != nil || opts.NamespaceEnabled != nil || opts.EnableHSTSHeaders
513+
leRequested := opts.WithLE
514+
isTopLevelDomain := isTopLevelSiteDomain(opts.Domain)
512515

513-
if !phpRequested && !opts.WithWordPress && !securityRequested {
516+
if !phpRequested && !opts.WithWordPress && !securityRequested && !leRequested {
514517
return apperr.New(apperr.CodeValidation, "no update action requested")
515518
}
516519
if opts.WithWordPress && !phpRequested {
@@ -528,23 +531,30 @@ func (s SiteService) UpdateSitePHP(ctx context.Context, opts UpdateSiteOptions)
528531
if err != nil {
529532
return err
530533
}
534+
packages = packagesForPHPUpdate(phpVersion)
535+
}
536+
if phpRequested || leRequested {
531537
info, err = s.detector.Detect(ctx)
532538
if err != nil {
533539
return err
534540
}
535-
packages = packagesForPHPUpdate(phpVersion)
536541
}
537542

538543
s.console.Section("Update site configuration")
539544
s.console.Bullet("Domain: " + opts.Domain)
540545
if phpRequested {
541546
s.console.Bullet("Target PHP: lsphp" + phpVersion)
547+
}
548+
if phpRequested || leRequested {
542549
s.console.Bullet("Platform: " + info.Summary())
543550
}
544551
s.console.Bullet("VHost config: " + vhostConfig)
545552
if opts.WithWordPress {
546553
s.console.Bullet("WordPress + LiteSpeed Cache reconcile: enabled")
547554
}
555+
if opts.WithLE {
556+
s.console.Bullet("Let's Encrypt: enabled")
557+
}
548558
if opts.OWASPEnabled != nil {
549559
s.console.Bullet("OWASP virtual-host mode: " + enabledLabel(*opts.OWASPEnabled))
550560
}
@@ -586,6 +596,20 @@ func (s SiteService) UpdateSitePHP(ctx context.Context, opts UpdateSiteOptions)
586596
if opts.EnableHSTSHeaders {
587597
s.console.Bullet("append recommended security extra headers to context / in " + vhostConfig)
588598
}
599+
if opts.WithLE {
600+
if isTopLevelDomain {
601+
s.console.Bullet("perform domain reachability precheck for " + opts.Domain)
602+
s.console.Bullet("perform optional www reachability precheck for www." + opts.Domain)
603+
s.console.Bullet("if www precheck passes, issue Let's Encrypt cert for both domains")
604+
s.console.Bullet("if www precheck fails, issue Let's Encrypt cert for primary domain only")
605+
} else {
606+
s.console.Bullet("skip domain reachability precheck for subdomain LE issuance")
607+
}
608+
s.console.Bullet("ensure certbot is installed")
609+
s.console.Bullet("issue Let's Encrypt certificate via certbot webroot challenge")
610+
s.console.Bullet("write cert/key into vhost SSL config")
611+
s.console.Bullet("reload OpenLiteSpeed")
612+
}
589613
s.console.Success("Dry-run plan generated")
590614
return nil
591615
}
@@ -643,6 +667,42 @@ func (s SiteService) UpdateSitePHP(ctx context.Context, opts UpdateSiteOptions)
643667
return err
644668
}
645669

670+
if opts.WithLE {
671+
certDomains := []string{opts.Domain}
672+
if isTopLevelDomain {
673+
ok, detail := precheckLEDomainReachability(opts.Domain)
674+
if !ok {
675+
return apperr.New(apperr.CodeValidation, "Let's Encrypt precheck failed for "+opts.Domain+": "+detail)
676+
}
677+
s.console.Success("Let's Encrypt precheck passed for " + opts.Domain + ": " + detail)
678+
679+
wwwDomain := "www." + opts.Domain
680+
okWWW, detailWWW := precheckLEDomainReachability(wwwDomain)
681+
if okWWW {
682+
certDomains = append(certDomains, wwwDomain)
683+
s.console.Success("Let's Encrypt precheck passed for " + wwwDomain + ": " + detailWWW)
684+
} else {
685+
s.console.Warn("Let's Encrypt precheck failed for " + wwwDomain + "; issuing certificate for primary domain only: " + detailWWW)
686+
}
687+
}
688+
689+
certFile, keyFile, err := s.issueLetsEncryptCertificate(ctx, info, docRoot, certDomains...)
690+
if err != nil {
691+
return err
692+
}
693+
if err := applyVHostSSLCertificate(vhostConfig, certFile, keyFile); err != nil {
694+
return err
695+
}
696+
s.console.Success("Let's Encrypt certificate issued")
697+
s.console.Bullet("Certificate domains: " + strings.Join(certDomains, ", "))
698+
s.console.Bullet("Certificate: " + certFile)
699+
s.console.Bullet("Private key: " + keyFile)
700+
701+
if err := s.reloadOpenLiteSpeed(ctx); err != nil {
702+
s.console.Warn("Failed to reload OpenLiteSpeed automatically after SSL issuance: " + err.Error())
703+
}
704+
}
705+
646706
if boolPtrValue(opts.RecaptchaEnabled) {
647707
recaptchaEnabled, err := isServerRecaptchaEnabled(serverConfigPath)
648708
if err != nil {
@@ -659,7 +719,9 @@ func (s SiteService) UpdateSitePHP(ctx context.Context, opts UpdateSiteOptions)
659719
if securityChanged {
660720
s.console.Success("Requested security options applied to vhost config")
661721
}
662-
s.console.Bullet("Reload OpenLiteSpeed to apply configuration changes")
722+
if !opts.WithLE {
723+
s.console.Bullet("Reload OpenLiteSpeed to apply configuration changes")
724+
}
663725
return nil
664726
}
665727

internal/service/site_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,95 @@ func TestUpdateSiteSecurityOnlyWithoutPHP(t *testing.T) {
382382
}
383383
}
384384

385+
func TestUpdateSiteLetsEncryptForSubdomainWithoutPHP(t *testing.T) {
386+
var out bytes.Buffer
387+
console := ui.NewStyledConsole(&out)
388+
r := &fakeRunner{}
389+
390+
base := t.TempDir()
391+
lswsRoot := filepath.Join(base, "lsws")
392+
webRoot := filepath.Join(base, "www")
393+
domain := "api.example.com"
394+
vhostDir := filepath.Join(lswsRoot, "conf", "vhosts", domain)
395+
396+
if err := os.MkdirAll(filepath.Join(lswsRoot, "bin"), 0o755); err != nil {
397+
t.Fatalf("mkdir bin: %v", err)
398+
}
399+
if err := os.MkdirAll(filepath.Join(lswsRoot, "conf"), 0o755); err != nil {
400+
t.Fatalf("mkdir conf: %v", err)
401+
}
402+
if err := os.MkdirAll(vhostDir, 0o755); err != nil {
403+
t.Fatalf("mkdir vhost: %v", err)
404+
}
405+
if err := os.WriteFile(filepath.Join(lswsRoot, "bin", "lswsctrl"), []byte("stub"), 0o755); err != nil {
406+
t.Fatalf("write lswsctrl: %v", err)
407+
}
408+
if err := os.WriteFile(filepath.Join(lswsRoot, "conf", "httpd_config.conf"), []byte("listener Default {\n}\n"), 0o644); err != nil {
409+
t.Fatalf("write server config: %v", err)
410+
}
411+
if err := os.WriteFile(filepath.Join(vhostDir, "vhconf.conf"), []byte(buildVHConfig("85")), 0o644); err != nil {
412+
t.Fatalf("write vhconf: %v", err)
413+
}
414+
415+
oldRoot := letsencryptLiveRoot
416+
letsencryptLiveRoot = filepath.Join(base, "letsencrypt", "live")
417+
t.Cleanup(func() { letsencryptLiveRoot = oldRoot })
418+
419+
certFile, keyFile := letsEncryptCertPaths(domain)
420+
if err := os.MkdirAll(filepath.Dir(certFile), 0o755); err != nil {
421+
t.Fatalf("mkdir cert dir: %v", err)
422+
}
423+
if err := os.WriteFile(certFile, []byte("cert"), 0o644); err != nil {
424+
t.Fatalf("write cert file: %v", err)
425+
}
426+
if err := os.WriteFile(keyFile, []byte("key"), 0o600); err != nil {
427+
t.Fatalf("write key file: %v", err)
428+
}
429+
430+
svc := NewSiteServiceWithPaths(
431+
fakeDetector{info: platform.Info{ID: "ubuntu", Family: platform.FamilyDebian, PackageManager: platform.PackageManagerAPT, VersionID: "24.04"}},
432+
r,
433+
console,
434+
lswsRoot,
435+
webRoot,
436+
)
437+
438+
err := svc.UpdateSitePHP(context.Background(), UpdateSiteOptions{Domain: domain, WithLE: true})
439+
if err != nil {
440+
t.Fatalf("unexpected update error: %v", err)
441+
}
442+
443+
updated, err := os.ReadFile(filepath.Join(vhostDir, "vhconf.conf"))
444+
if err != nil {
445+
t.Fatalf("read vhconf: %v", err)
446+
}
447+
content := string(updated)
448+
if !strings.Contains(content, certFile) || !strings.Contains(content, keyFile) {
449+
t.Fatalf("expected updated SSL cert/key paths in vhconf, got: %s", content)
450+
}
451+
452+
hasCertbot := false
453+
hasReload := false
454+
for _, call := range r.calls {
455+
if len(call) > 0 && call[0] == "certbot" {
456+
hasCertbot = true
457+
joined := strings.Join(call, " ")
458+
if !strings.Contains(joined, "-d "+domain) {
459+
t.Fatalf("expected certbot args to include -d %s, got: %#v", domain, call)
460+
}
461+
}
462+
if len(call) == 2 && filepath.Base(call[0]) == "lswsctrl" && call[1] == "reload" {
463+
hasReload = true
464+
}
465+
}
466+
if !hasCertbot {
467+
t.Fatalf("expected certbot call, got calls: %#v", r.calls)
468+
}
469+
if !hasReload {
470+
t.Fatalf("expected lswsctrl reload call, got calls: %#v", r.calls)
471+
}
472+
}
473+
385474
func TestApplyVHostSecurityOptionsEnableAndDisable(t *testing.T) {
386475
vhostPath := filepath.Join(t.TempDir(), "vhconf.conf")
387476
if err := os.WriteFile(vhostPath, []byte(buildVHConfig("85")), 0o644); err != nil {

0 commit comments

Comments
 (0)