mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 16:11:29 +00:00
36d79cd1ff
Phase 8 of the deploy-hardening I master bundle. F5 + IIS already have transactional / explicit-backup-restore rollback semantics in their DeployCertificate paths. Phase 8 adds the explicit ValidateOnly dry-run probe that operators use to preview a deploy without touching the live cert. F5 (validate_only.go): - ValidateOnly probes the iControl REST API via Authenticate. Cheap (no F5 transaction created) + cached after first success. Failure surfaces as a wrapped error so operators see the actual cause (auth provider down, invalid creds, BIG-IP unreachable, etc.). nil client returns ErrValidateOnlyNotSupported. - A true cert-bind dry-run requires F5's no-commit transaction mode (v17.5+); V3-Pro can add per-version dispatch. V2 ships the reachability probe as the load-bearing safety check. - 5 new tests in validate_only_test.go covering: auth-success, auth-fail wrapped, nil-client sentinel, error-message contains BIG-IP context, recoverable auth-fail surfaces provider info. IIS (validate_only.go): - ValidateOnly runs `Get-WebSite -Name <SiteName>` via the injected PowerShellExecutor. Confirms the IIS PS module is loaded AND the site exists AND the agent has admin privileges. Failure here surfaces the actual PowerShell stderr (site not found / module missing / access denied). - A true cert-bind dry-run would need IIS to expose a no-commit New-WebBinding (it doesn't); V3-Pro can extend with a temp-install + immediate-remove. V2 ships the permission + module probe as the load-bearing check. - 5 new tests in validate_only_test.go covering: get-website succeeds, get-website fails, nil-executor sentinel, site-name quoting (handles spaces in 'Default Web Site'), output-context in error. Smoke test connectorsAtPhase3 list shrunk from 10 to 7 entries (f5 + iis + postfix removed). Caddy stays in (file-mode returns sentinel; api-mode is real-impl). Envoy + Traefik stay in (no validate-with-target command exists for either). javakeystore + k8ssecret + ssh + wincertstore stay in pending Phase 9. Coverage: F5 holds at ≥85%; IIS holds at ≥85%. Race detector clean. golangci-lint v2.11.4 clean. Phase 9 next: SSH + WinCertStore + JavaKeystore + K8s — the non-file-server connectors.
97 lines
3.1 KiB
Go
97 lines
3.1 KiB
Go
package iis
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/shankar0123/certctl/internal/connector/target"
|
|
)
|
|
|
|
// Phase 8 of the deploy-hardening I master bundle: IIS ValidateOnly
|
|
// real implementation tests. IIS already has explicit pre-deploy
|
|
// backup + post-rollback re-import semantics; the new bit is the
|
|
// PowerShell health probe via Get-WebSite.
|
|
|
|
type stubExecutor struct {
|
|
out string
|
|
err error
|
|
}
|
|
|
|
func (s *stubExecutor) Execute(_ context.Context, _ string) (string, error) {
|
|
return s.out, s.err
|
|
}
|
|
|
|
func quietLogger() *slog.Logger {
|
|
return slog.New(slog.NewTextHandler(os.NewFile(0, os.DevNull), &slog.HandlerOptions{Level: slog.LevelError}))
|
|
}
|
|
|
|
func TestIIS_ValidateOnly_GetWebSite_Succeeds(t *testing.T) {
|
|
c := NewWithExecutor(&Config{SiteName: "Default Web Site"}, quietLogger(), &stubExecutor{out: "Default Web Site"})
|
|
if err := c.ValidateOnly(context.Background(), target.DeploymentRequest{}); err != nil {
|
|
t.Errorf("got %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestIIS_ValidateOnly_GetWebSite_Fails(t *testing.T) {
|
|
c := NewWithExecutor(&Config{SiteName: "Missing"}, quietLogger(), &stubExecutor{
|
|
out: "Get-WebSite : Cannot find a Web site with name 'Missing'",
|
|
err: errors.New("PowerShell exit 1"),
|
|
})
|
|
err := c.ValidateOnly(context.Background(), target.DeploymentRequest{})
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
if errors.Is(err, target.ErrValidateOnlyNotSupported) {
|
|
t.Errorf("got sentinel, want wrapped error: %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "Cannot find") {
|
|
t.Errorf("error missing PowerShell stderr: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestIIS_ValidateOnly_NilExecutor_ReturnsSentinel(t *testing.T) {
|
|
c := &Connector{config: &Config{SiteName: "x"}, logger: quietLogger()}
|
|
if err := c.ValidateOnly(context.Background(), target.DeploymentRequest{}); !errors.Is(err, target.ErrValidateOnlyNotSupported) {
|
|
t.Errorf("got %v, want sentinel", err)
|
|
}
|
|
}
|
|
|
|
func TestIIS_ValidateOnly_SiteNameQuoted(t *testing.T) {
|
|
// Verify the script PROPERLY quotes site names with spaces (a common
|
|
// IIS site name pattern).
|
|
captured := ""
|
|
exec := &stubExecutor{out: "Default Web Site"}
|
|
c := NewWithExecutor(&Config{SiteName: "Default Web Site"}, quietLogger(), exec)
|
|
// Wrap exec to capture script.
|
|
c.executor = captureExec{wrapped: exec, captured: &captured}
|
|
c.ValidateOnly(context.Background(), target.DeploymentRequest{})
|
|
if !strings.Contains(captured, `"Default Web Site"`) {
|
|
t.Errorf("script missing quoted site name: %q", captured)
|
|
}
|
|
}
|
|
|
|
type captureExec struct {
|
|
wrapped PowerShellExecutor
|
|
captured *string
|
|
}
|
|
|
|
func (c captureExec) Execute(ctx context.Context, script string) (string, error) {
|
|
*c.captured = script
|
|
return c.wrapped.Execute(ctx, script)
|
|
}
|
|
|
|
func TestIIS_ValidateOnly_OutputContextInError(t *testing.T) {
|
|
c := NewWithExecutor(&Config{SiteName: "DWS"}, quietLogger(), &stubExecutor{
|
|
out: "WARNING: This site is in stopped state",
|
|
err: errors.New("exit 1"),
|
|
})
|
|
err := c.ValidateOnly(context.Background(), target.DeploymentRequest{})
|
|
if err == nil || !strings.Contains(err.Error(), "stopped state") {
|
|
t.Errorf("got %v", err)
|
|
}
|
|
}
|