Skip to content
Merged
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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,22 @@ to avoid the "I forgot the key and lost my zone" failure mode.
`wfctl secrets setup --plugin workflow-plugin-hover` prompts for each;
sensitive fields are masked.

## Importing existing state

The plugin supports read-only import for existing Hover domains:

```sh
wfctl infra import --config infra.yaml --name example-com-dns --id example.com
wfctl infra import --config infra.yaml --name example-com-delegation --id example.com
```

Declare the target resource in config first so `wfctl` can resolve the Hover
provider and resource type. `infra.dns` imports the zone records returned by
Hover. `infra.dns_delegation` imports the current registrar nameservers.
Imported state is marked as adoption-shaped state so follow-up plans can
compare against live outputs without treating the imported record set as a
user-authored apply config.

## TOTP

In-process RFC 6238 (SHA-1, 30s step, 6 digits). The seed is decoded
Expand Down
8 changes: 6 additions & 2 deletions internal/iacserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,15 @@ func (s *hoverIaCServer) Status(ctx context.Context, req *pb.StatusRequest) (*pb
}

func (s *hoverIaCServer) Import(ctx context.Context, req *pb.ImportRequest) (*pb.ImportResponse, error) {
_, err := s.provider.Import(ctx, req.GetProviderId(), req.GetResourceType())
state, err := s.provider.Import(ctx, req.GetProviderId(), req.GetResourceType())
if err != nil {
return nil, err
}
return &pb.ImportResponse{}, nil
pbState, err := stateToPB(state)
if err != nil {
return nil, fmt.Errorf("hover iacserver: encode Import state: %w", err)
}
return &pb.ImportResponse{State: pbState}, nil
}

func (s *hoverIaCServer) ResolveSizing(_ context.Context, _ *pb.ResolveSizingRequest) (*pb.ResolveSizingResponse, error) {
Expand Down
53 changes: 53 additions & 0 deletions internal/iacserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,59 @@ func TestHoverIaCServer_ResourceDriverReadAndDiff(t *testing.T) {
}
}

func TestHoverIaCServer_Import_ReturnsResourceState(t *testing.T) {
driver := &iacServerFakeDriver{
readOut: &interfaces.ResourceOutput{
Name: "example.com",
Type: "infra.dns",
ProviderID: "example.com",
Outputs: map[string]any{
"domain": "example.com",
"records": []any{
map[string]any{"type": "MX", "name": "@", "content": "mail.protonmail.ch", "ttl": 300},
},
},
Status: "active",
},
}
srv := &hoverIaCServer{
provider: &HoverProvider{drivers: map[string]interfaces.ResourceDriver{
"infra.dns": driver,
}},
}

resp, err := srv.Import(context.Background(), &pb.ImportRequest{ProviderId: "example.com"})
if err != nil {
t.Fatalf("Import: %v", err)
}
state := resp.GetState()
if state == nil {
t.Fatal("Import state is nil")
}
if state.GetProvider() != "hover" {
t.Fatalf("Provider = %q, want hover", state.GetProvider())
}
if state.GetType() != "infra.dns" {
t.Fatalf("Type = %q, want infra.dns", state.GetType())
}
if state.GetProviderId() != "example.com" {
t.Fatalf("ProviderId = %q, want example.com", state.GetProviderId())
}
if state.GetAppliedConfigSource() != "adoption" {
t.Fatalf("AppliedConfigSource = %q, want adoption", state.GetAppliedConfigSource())
}
outputs, err := unmarshalJSONMap(state.GetOutputsJson())
if err != nil {
t.Fatalf("outputs JSON: %v", err)
}
if outputs["domain"] != "example.com" {
t.Fatalf("outputs.domain = %v, want example.com", outputs["domain"])
}
if driver.readRef != (interfaces.ResourceRef{Name: "example.com", Type: "infra.dns", ProviderID: "example.com"}) {
t.Fatalf("driver read ref = %+v, want provider-id import ref", driver.readRef)
}
}

func TestHoverIaCServer_Destroy_EmptyRefs(t *testing.T) {
srv := NewIaCServer()
// Destroy with zero refs is a no-op regardless of initialization state.
Expand Down
40 changes: 37 additions & 3 deletions internal/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"strings"
"time"

"github.com/GoCodeAlone/workflow-plugin-hover/internal/drivers"
"github.com/GoCodeAlone/workflow-plugin-hover/internal/hover"
Expand Down Expand Up @@ -222,9 +223,42 @@ func (p *HoverProvider) DetectDrift(ctx context.Context, resources []interfaces.
return results, nil
}

// Import is a stub: Hover does not support resource import via cloud ID.
func (p *HoverProvider) Import(_ context.Context, _ string, _ string) (*interfaces.ResourceState, error) {
return nil, fmt.Errorf("hover: Import is not supported")
// Import reads an existing Hover-managed resource and returns IaC adoption
// state. cloudID is the domain name for both infra.dns and infra.dns_delegation.
func (p *HoverProvider) Import(ctx context.Context, cloudID string, resourceType string) (*interfaces.ResourceState, error) {
if cloudID == "" {
return nil, fmt.Errorf("hover import: provider_id is required")
}
if resourceType == "" {
resourceType = "infra.dns"
}
d, err := p.ResourceDriver(resourceType)
if err != nil {
return nil, err
}
out, err := d.Read(ctx, interfaces.ResourceRef{Name: cloudID, Type: resourceType, ProviderID: cloudID})
if err != nil {
return nil, fmt.Errorf("hover import %q: %w", cloudID, err)
}
if out == nil {
return nil, fmt.Errorf("hover import %q: driver returned nil output", cloudID)
}
now := time.Now()
id := out.ProviderID
if id == "" {
id = cloudID
}
return &interfaces.ResourceState{
ID: id,
Name: out.Name,
Type: out.Type,
Provider: "hover",
ProviderID: id,
AppliedConfigSource: "adoption",
Outputs: out.Outputs,
CreatedAt: now,
UpdatedAt: now,
}, nil
}

// ResolveSizing is a stub: Hover has no compute sizing.
Expand Down