Skip to content

Commit 60eeb28

Browse files
committed
enable/disable
1 parent 633254a commit 60eeb28

5 files changed

Lines changed: 299 additions & 1 deletion

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ ols (command) [options]
111111
| Command | Purpose | Options |
112112
| --- | --- | --- |
113113
| `install` | Install/align OpenLiteSpeed runtime and related packages | `--php81` `--php82` `--php83` `--php84` `--php85` `--database` `--config` `--http-port` `--https-port` `--ssl-cert` `--ssl-key` `--no-listeners` |
114-
| `site` | Manage sites (`create`, `update`, `info`, `show`, `list`, `delete`) | `--wp` `--le` `--php81` `--php82` `--php83` `--php84` `--php85` `--enable-owasp` `--disable-owasp` `--enable-recaptcha` `--disable-recaptcha` `--enable-ns` `--disable-ns` `--hsts` `--keep-db` |
114+
| `site` | Manage sites (`create`, `update`, `enable`, `disable`, `info`, `show`, `list`, `delete`) | `--wp` `--le` `--php81` `--php82` `--php83` `--php84` `--php85` `--enable-owasp` `--disable-owasp` `--enable-recaptcha` `--disable-recaptcha` `--enable-ns` `--disable-ns` `--hsts` `--keep-db` |
115115
| `update` | Update installed `ols` binary to latest GitHub release for current platform | |
116116

117117
Global options (apply to all commands): `--dry-run`, `--color`
@@ -126,6 +126,8 @@ ols site (command) [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` |
128128
| `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` |
129+
| `enable` | Enable site by removing domain from server-level `suspendedVhosts` | |
130+
| `disable` | Disable site by adding domain to server-level `suspendedVhosts` | |
129131
| `info` | Show site metadata and detected status | |
130132
| `show` | Print OLS virtual host config (`vhconf.conf`) | |
131133
| `list` | List managed sites discovered from OLS vhost directory | |

internal/cli/site.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
type siteManager interface {
1313
CreateSite(ctx context.Context, opts service.CreateSiteOptions) error
1414
UpdateSitePHP(ctx context.Context, opts service.UpdateSiteOptions) error
15+
EnableSite(ctx context.Context, opts service.ToggleSiteOptions) error
16+
DisableSite(ctx context.Context, opts service.ToggleSiteOptions) error
1517
SiteInfo(ctx context.Context, opts service.SiteInfoOptions) error
1618
ShowSiteConfig(ctx context.Context, opts service.ShowSiteConfigOptions) error
1719
ListSites(ctx context.Context, opts service.ListSitesOptions) error
@@ -96,6 +98,8 @@ func newSiteCmd(svc siteManager, rootOpts *rootOptions) *cobra.Command {
9698

9799
siteCmd.AddCommand(newSiteCreateCmd(svc, rootOpts))
98100
siteCmd.AddCommand(newSiteUpdateCmd(svc, rootOpts))
101+
siteCmd.AddCommand(newSiteEnableCmd(svc, rootOpts))
102+
siteCmd.AddCommand(newSiteDisableCmd(svc, rootOpts))
99103
siteCmd.AddCommand(newSiteInfoCmd(svc, rootOpts))
100104
siteCmd.AddCommand(newSiteShowCmd(svc, rootOpts))
101105
siteCmd.AddCommand(newSiteListCmd(svc, rootOpts))
@@ -242,6 +246,46 @@ func newSiteUpdateCmd(svc siteManager, rootOpts *rootOptions) *cobra.Command {
242246
return cmd
243247
}
244248

249+
func newSiteEnableCmd(svc siteManager, rootOpts *rootOptions) *cobra.Command {
250+
cmd := &cobra.Command{
251+
Use: "enable <domain>",
252+
Short: "Enable site in OpenLiteSpeed server config",
253+
Example: "ols site enable example.com",
254+
Args: cobra.ExactArgs(1),
255+
RunE: func(cmd *cobra.Command, args []string) error {
256+
err := svc.EnableSite(cmd.Context(), service.ToggleSiteOptions{
257+
Domain: args[0],
258+
DryRun: rootOpts.DryRun,
259+
})
260+
if err != nil {
261+
return fmt.Errorf("site enable failed: %w", err)
262+
}
263+
return nil
264+
},
265+
}
266+
return cmd
267+
}
268+
269+
func newSiteDisableCmd(svc siteManager, rootOpts *rootOptions) *cobra.Command {
270+
cmd := &cobra.Command{
271+
Use: "disable <domain>",
272+
Short: "Disable site in OpenLiteSpeed server config",
273+
Example: "ols site disable example.com",
274+
Args: cobra.ExactArgs(1),
275+
RunE: func(cmd *cobra.Command, args []string) error {
276+
err := svc.DisableSite(cmd.Context(), service.ToggleSiteOptions{
277+
Domain: args[0],
278+
DryRun: rootOpts.DryRun,
279+
})
280+
if err != nil {
281+
return fmt.Errorf("site disable failed: %w", err)
282+
}
283+
return nil
284+
},
285+
}
286+
return cmd
287+
}
288+
245289
func newSiteInfoCmd(svc siteManager, rootOpts *rootOptions) *cobra.Command {
246290
cmd := &cobra.Command{
247291
Use: "info <domain>",

internal/cli/site_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ func (noopSiteManager) UpdateSitePHP(context.Context, service.UpdateSiteOptions)
1818
return nil
1919
}
2020

21+
func (noopSiteManager) EnableSite(context.Context, service.ToggleSiteOptions) error {
22+
return nil
23+
}
24+
25+
func (noopSiteManager) DisableSite(context.Context, service.ToggleSiteOptions) error {
26+
return nil
27+
}
28+
2129
func (noopSiteManager) SiteInfo(context.Context, service.SiteInfoOptions) error {
2230
return nil
2331
}
@@ -167,3 +175,23 @@ func TestSiteUpdateFlagOrder(t *testing.T) {
167175
}
168176
}
169177
}
178+
179+
func TestSiteCommandIncludesEnableDisable(t *testing.T) {
180+
cmd := newSiteCmd(noopSiteManager{}, &rootOptions{})
181+
182+
enableCmd, _, err := cmd.Find([]string{"enable"})
183+
if err != nil {
184+
t.Fatalf("expected enable command, got error: %v", err)
185+
}
186+
if enableCmd == nil || enableCmd.Name() != "enable" {
187+
t.Fatalf("expected enable command to be registered, got: %#v", enableCmd)
188+
}
189+
190+
disableCmd, _, err := cmd.Find([]string{"disable"})
191+
if err != nil {
192+
t.Fatalf("expected disable command, got error: %v", err)
193+
}
194+
if disableCmd == nil || disableCmd.Name() != "disable" {
195+
t.Fatalf("expected disable command to be registered, got: %#v", disableCmd)
196+
}
197+
}

internal/service/site.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,11 @@ type DeleteSiteOptions struct {
150150
DryRun bool
151151
}
152152

153+
type ToggleSiteOptions struct {
154+
Domain string
155+
DryRun bool
156+
}
157+
153158
type SiteInfoOptions struct {
154159
Domain string
155160
DryRun bool
@@ -789,6 +794,186 @@ func (s SiteService) DeleteSite(ctx context.Context, opts DeleteSiteOptions) err
789794
return nil
790795
}
791796

797+
func (s SiteService) DisableSite(ctx context.Context, opts ToggleSiteOptions) error {
798+
if err := ValidateDomain(opts.Domain); err != nil {
799+
return err
800+
}
801+
802+
domain := strings.ToLower(strings.TrimSpace(opts.Domain))
803+
vhostConfig := filepath.Join(s.lswsRoot, "conf", "vhosts", domain, "vhconf.conf")
804+
serverConfigPath := filepath.Join(s.lswsRoot, "conf", "httpd_config.conf")
805+
806+
s.console.Section("Disable site")
807+
s.console.Bullet("Domain: " + domain)
808+
s.console.Bullet("Server config: " + serverConfigPath)
809+
810+
if opts.DryRun {
811+
s.console.Warn("Dry-run enabled: no system changes were made")
812+
s.console.Bullet("append " + domain + " to suspendedVhosts in " + serverConfigPath)
813+
s.console.Bullet("reload OpenLiteSpeed")
814+
s.console.Success("Dry-run plan generated")
815+
return nil
816+
}
817+
818+
if !fileExists(vhostConfig) {
819+
return apperr.New(apperr.CodeValidation, fmt.Sprintf("virtual host does not exist for %s; expected %s", domain, vhostConfig))
820+
}
821+
if err := s.setSiteSuspended(domain, true, serverConfigPath); err != nil {
822+
return err
823+
}
824+
if err := s.reloadOpenLiteSpeed(ctx); err != nil {
825+
s.console.Warn("Failed to reload OpenLiteSpeed automatically: " + err.Error())
826+
}
827+
828+
s.console.Success("Site disabled")
829+
return nil
830+
}
831+
832+
func (s SiteService) EnableSite(ctx context.Context, opts ToggleSiteOptions) error {
833+
if err := ValidateDomain(opts.Domain); err != nil {
834+
return err
835+
}
836+
837+
domain := strings.ToLower(strings.TrimSpace(opts.Domain))
838+
vhostConfig := filepath.Join(s.lswsRoot, "conf", "vhosts", domain, "vhconf.conf")
839+
serverConfigPath := filepath.Join(s.lswsRoot, "conf", "httpd_config.conf")
840+
841+
s.console.Section("Enable site")
842+
s.console.Bullet("Domain: " + domain)
843+
s.console.Bullet("Server config: " + serverConfigPath)
844+
845+
if opts.DryRun {
846+
s.console.Warn("Dry-run enabled: no system changes were made")
847+
s.console.Bullet("remove " + domain + " from suspendedVhosts in " + serverConfigPath)
848+
s.console.Bullet("reload OpenLiteSpeed")
849+
s.console.Success("Dry-run plan generated")
850+
return nil
851+
}
852+
853+
if !fileExists(vhostConfig) {
854+
return apperr.New(apperr.CodeValidation, fmt.Sprintf("virtual host does not exist for %s; expected %s", domain, vhostConfig))
855+
}
856+
if err := s.setSiteSuspended(domain, false, serverConfigPath); err != nil {
857+
return err
858+
}
859+
if err := s.reloadOpenLiteSpeed(ctx); err != nil {
860+
s.console.Warn("Failed to reload OpenLiteSpeed automatically: " + err.Error())
861+
}
862+
863+
s.console.Success("Site enabled")
864+
return nil
865+
}
866+
867+
func (s SiteService) setSiteSuspended(domain string, suspended bool, serverConfigPath string) error {
868+
b, err := os.ReadFile(serverConfigPath)
869+
if err != nil {
870+
return apperr.Wrap(apperr.CodeConfig, "failed to read OpenLiteSpeed server config", err)
871+
}
872+
873+
cfg, changed := updateSuspendedVhostsDirective(string(b), domain, suspended)
874+
if !changed {
875+
if suspended {
876+
s.console.Bullet("Site already disabled: " + domain)
877+
} else {
878+
s.console.Bullet("Site already enabled: " + domain)
879+
}
880+
return nil
881+
}
882+
883+
if err := os.WriteFile(serverConfigPath, []byte(cfg), 0o644); err != nil {
884+
return apperr.Wrap(apperr.CodeConfig, "failed to update OpenLiteSpeed server config", err)
885+
}
886+
887+
if suspended {
888+
s.console.Bullet("Added to suspendedVhosts: " + domain)
889+
} else {
890+
s.console.Bullet("Removed from suspendedVhosts: " + domain)
891+
}
892+
return nil
893+
}
894+
895+
func updateSuspendedVhostsDirective(cfg, domain string, suspended bool) (string, bool) {
896+
lines := strings.Split(cfg, "\n")
897+
normalizedDomain := strings.ToLower(strings.TrimSpace(domain))
898+
if normalizedDomain == "" {
899+
return cfg, false
900+
}
901+
902+
for i, line := range lines {
903+
trimmed := strings.TrimSpace(line)
904+
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
905+
continue
906+
}
907+
fields := strings.Fields(trimmed)
908+
if len(fields) == 0 || fields[0] != "suspendedVhosts" {
909+
continue
910+
}
911+
912+
current := parseSuspendedVhosts(fields[1:])
913+
updated, changed := toggleSuspendedDomain(current, normalizedDomain, suspended)
914+
if !changed {
915+
return cfg, false
916+
}
917+
if len(updated) == 0 {
918+
lines = append(lines[:i], lines[i+1:]...)
919+
return strings.Join(lines, "\n"), true
920+
}
921+
922+
indent := ""
923+
if idx := strings.Index(line, fields[0]); idx > 0 {
924+
indent = line[:idx]
925+
}
926+
lines[i] = formatDirectiveLine(indent, "suspendedVhosts", strings.Join(updated, ","))
927+
return strings.Join(lines, "\n"), true
928+
}
929+
930+
if !suspended {
931+
return cfg, false
932+
}
933+
934+
updated := append([]string{}, lines...)
935+
updated, _ = upsertDirective(updated, "suspendedVhosts", normalizedDomain)
936+
return strings.Join(updated, "\n"), true
937+
}
938+
939+
func parseSuspendedVhosts(tokens []string) []string {
940+
parsed := make([]string, 0, len(tokens))
941+
for _, token := range tokens {
942+
for _, host := range strings.Split(token, ",") {
943+
host = strings.ToLower(strings.TrimSpace(host))
944+
if host == "" {
945+
continue
946+
}
947+
parsed = append(parsed, host)
948+
}
949+
}
950+
return uniqueNormalizedDomains(parsed)
951+
}
952+
953+
func toggleSuspendedDomain(domains []string, domain string, suspended bool) ([]string, bool) {
954+
normalized := uniqueNormalizedDomains(domains)
955+
if suspended {
956+
for _, existing := range normalized {
957+
if existing == domain {
958+
return normalized, false
959+
}
960+
}
961+
normalized = append(normalized, domain)
962+
return normalized, true
963+
}
964+
965+
out := make([]string, 0, len(normalized))
966+
removed := false
967+
for _, existing := range normalized {
968+
if existing == domain {
969+
removed = true
970+
continue
971+
}
972+
out = append(out, existing)
973+
}
974+
return out, removed
975+
}
976+
792977
func (s SiteService) ShowSiteConfig(_ context.Context, opts ShowSiteConfigOptions) error {
793978
if err := ValidateDomain(opts.Domain); err != nil {
794979
return err

internal/service/site_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,3 +885,42 @@ func TestCertbotPackagesFor(t *testing.T) {
885885
t.Fatalf("expected no package for unknown package manager, got: %v", got)
886886
}
887887
}
888+
889+
func TestUpdateSuspendedVhostsDirectiveAddWhenMissing(t *testing.T) {
890+
cfg := "listener Default {\n address *:80\n}\n"
891+
892+
updated, changed := updateSuspendedVhostsDirective(cfg, "litespeedtech.club", true)
893+
if !changed {
894+
t.Fatal("expected config to change when disabling site with missing suspendedVhosts")
895+
}
896+
if !strings.Contains(updated, "suspendedVhosts") || !strings.Contains(updated, "litespeedtech.club") {
897+
t.Fatalf("expected suspendedVhosts to include disabled domain, got: %s", updated)
898+
}
899+
}
900+
901+
func TestUpdateSuspendedVhostsDirectiveRemoveSpecificDomain(t *testing.T) {
902+
cfg := "suspendedVhosts litespeedtech.club,cli2.litespeedtech.club\nlistener Default {\n address *:80\n}\n"
903+
904+
updated, changed := updateSuspendedVhostsDirective(cfg, "litespeedtech.club", false)
905+
if !changed {
906+
t.Fatal("expected config to change when enabling a suspended site")
907+
}
908+
if strings.Contains(updated, "litespeedtech.club,") || strings.Contains(updated, ",litespeedtech.club") || strings.Contains(updated, " litespeedtech.club\n") {
909+
t.Fatalf("expected enabled domain removed from suspendedVhosts, got: %s", updated)
910+
}
911+
if !strings.Contains(updated, "suspendedVhosts") || !strings.Contains(updated, "cli2.litespeedtech.club") {
912+
t.Fatalf("expected other suspended domains preserved, got: %s", updated)
913+
}
914+
}
915+
916+
func TestUpdateSuspendedVhostsDirectiveRemoveDirectiveWhenEmpty(t *testing.T) {
917+
cfg := "suspendedVhosts litespeedtech.club\nlistener Default {\n address *:80\n}\n"
918+
919+
updated, changed := updateSuspendedVhostsDirective(cfg, "litespeedtech.club", false)
920+
if !changed {
921+
t.Fatal("expected config to change when enabling last suspended site")
922+
}
923+
if strings.Contains(updated, "suspendedVhosts") {
924+
t.Fatalf("expected suspendedVhosts directive removed when empty, got: %s", updated)
925+
}
926+
}

0 commit comments

Comments
 (0)