diff --git a/internal/iacserver.go b/internal/iacserver.go index af698cf..2474901 100644 --- a/internal/iacserver.go +++ b/internal/iacserver.go @@ -186,6 +186,138 @@ func (s *hoverIaCServer) DetectDrift(ctx context.Context, req *pb.DetectDriftReq return &pb.DetectDriftResponse{Drifts: pbDrifts}, nil } +// ── ResourceDriver service ─────────────────────────────────────────────────── + +func (s *hoverIaCServer) Create(ctx context.Context, req *pb.ResourceCreateRequest) (*pb.ResourceCreateResponse, error) { + driver, err := s.provider.ResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + spec, err := specFromPB(req.GetSpec()) + if err != nil { + return nil, fmt.Errorf("hover iacserver: decode Create spec: %w", err) + } + out, err := driver.Create(ctx, spec) + if err != nil { + return nil, err + } + pbOut, err := outputToPB(out) + if err != nil { + return nil, fmt.Errorf("hover iacserver: encode Create output: %w", err) + } + return &pb.ResourceCreateResponse{Output: pbOut}, nil +} + +func (s *hoverIaCServer) Read(ctx context.Context, req *pb.ResourceReadRequest) (*pb.ResourceReadResponse, error) { + driver, err := s.provider.ResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + out, err := driver.Read(ctx, refFromPB(req.GetRef())) + if err != nil { + return nil, err + } + pbOut, err := outputToPB(out) + if err != nil { + return nil, fmt.Errorf("hover iacserver: encode Read output: %w", err) + } + return &pb.ResourceReadResponse{Output: pbOut}, nil +} + +func (s *hoverIaCServer) Update(ctx context.Context, req *pb.ResourceUpdateRequest) (*pb.ResourceUpdateResponse, error) { + driver, err := s.provider.ResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + spec, err := specFromPB(req.GetSpec()) + if err != nil { + return nil, fmt.Errorf("hover iacserver: decode Update spec: %w", err) + } + out, err := driver.Update(ctx, refFromPB(req.GetRef()), spec) + if err != nil { + return nil, err + } + pbOut, err := outputToPB(out) + if err != nil { + return nil, fmt.Errorf("hover iacserver: encode Update output: %w", err) + } + return &pb.ResourceUpdateResponse{Output: pbOut}, nil +} + +func (s *hoverIaCServer) Delete(ctx context.Context, req *pb.ResourceDeleteRequest) (*pb.ResourceDeleteResponse, error) { + driver, err := s.provider.ResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + if err := driver.Delete(ctx, refFromPB(req.GetRef())); err != nil { + return nil, err + } + return &pb.ResourceDeleteResponse{}, nil +} + +func (s *hoverIaCServer) Diff(ctx context.Context, req *pb.ResourceDiffRequest) (*pb.ResourceDiffResponse, error) { + driver, err := s.provider.ResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + spec, err := specFromPB(req.GetDesired()) + if err != nil { + return nil, fmt.Errorf("hover iacserver: decode Diff desired: %w", err) + } + current, err := outputFromPB(req.GetCurrent()) + if err != nil { + return nil, fmt.Errorf("hover iacserver: decode Diff current: %w", err) + } + diff, err := driver.Diff(ctx, spec, current) + if err != nil { + return nil, err + } + pbDiff, err := diffResultToPB(diff) + if err != nil { + return nil, fmt.Errorf("hover iacserver: encode Diff result: %w", err) + } + return &pb.ResourceDiffResponse{Result: pbDiff}, nil +} + +func (s *hoverIaCServer) Scale(ctx context.Context, req *pb.ResourceScaleRequest) (*pb.ResourceScaleResponse, error) { + driver, err := s.provider.ResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + out, err := driver.Scale(ctx, refFromPB(req.GetRef()), int(req.GetReplicas())) + if err != nil { + return nil, err + } + pbOut, err := outputToPB(out) + if err != nil { + return nil, fmt.Errorf("hover iacserver: encode Scale output: %w", err) + } + return &pb.ResourceScaleResponse{Output: pbOut}, nil +} + +func (s *hoverIaCServer) HealthCheck(ctx context.Context, req *pb.ResourceHealthCheckRequest) (*pb.ResourceHealthCheckResponse, error) { + driver, err := s.provider.ResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + result, err := driver.HealthCheck(ctx, refFromPB(req.GetRef())) + if err != nil { + return nil, err + } + if result == nil { + return &pb.ResourceHealthCheckResponse{}, nil + } + return &pb.ResourceHealthCheckResponse{Result: &pb.HealthResult{Healthy: result.Healthy, Message: result.Message}}, nil +} + +func (s *hoverIaCServer) SensitiveKeys(_ context.Context, req *pb.SensitiveKeysRequest) (*pb.SensitiveKeysResponse, error) { + driver, err := s.provider.ResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + return &pb.SensitiveKeysResponse{Keys: append([]string(nil), driver.SensitiveKeys()...)}, nil +} + // ── Marshalling helpers ─────────────────────────────────────────────────────── func unmarshalJSONMap(b []byte) (map[string]any, error) { @@ -469,6 +601,68 @@ func statusesToPB(ss []interfaces.ResourceStatus) ([]*pb.ResourceStatus, error) return out, nil } +func outputToPB(o *interfaces.ResourceOutput) (*pb.ResourceOutput, error) { + if o == nil { + return nil, nil + } + outputsJSON, err := marshalJSONMap(o.Outputs) + if err != nil { + return nil, err + } + return &pb.ResourceOutput{ + Name: o.Name, + Type: o.Type, + ProviderId: o.ProviderID, + OutputsJson: outputsJSON, + Sensitive: copyBoolMap(o.Sensitive), + Status: o.Status, + }, nil +} + +func outputFromPB(o *pb.ResourceOutput) (*interfaces.ResourceOutput, error) { + if o == nil { + return nil, nil + } + outputs, err := unmarshalJSONMap(o.GetOutputsJson()) + if err != nil { + return nil, err + } + return &interfaces.ResourceOutput{ + Name: o.GetName(), + Type: o.GetType(), + ProviderID: o.GetProviderId(), + Outputs: outputs, + Sensitive: copyBoolMap(o.GetSensitive()), + Status: o.GetStatus(), + }, nil +} + +func diffResultToPB(d *interfaces.DiffResult) (*pb.DiffResult, error) { + if d == nil { + return nil, nil + } + changes, err := changesToPB(d.Changes) + if err != nil { + return nil, err + } + return &pb.DiffResult{ + NeedsUpdate: d.NeedsUpdate, + NeedsReplace: d.NeedsReplace, + Changes: changes, + }, nil +} + +func copyBoolMap(in map[string]bool) map[string]bool { + if len(in) == 0 { + return nil + } + out := make(map[string]bool, len(in)) + for k, v := range in { + out[k] = v + } + return out +} + func driftClassToPB(c interfaces.DriftClass) pb.DriftClass { switch c { case interfaces.DriftClassInSync: diff --git a/internal/iacserver_test.go b/internal/iacserver_test.go index 386de04..a5154fb 100644 --- a/internal/iacserver_test.go +++ b/internal/iacserver_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/GoCodeAlone/workflow/interfaces" pb "github.com/GoCodeAlone/workflow/plugin/external/proto" ) @@ -106,6 +107,57 @@ func TestHoverIaCServer_Plan_EmptyDesired(t *testing.T) { } } +func TestHoverIaCServer_ResourceDriverReadAndDiff(t *testing.T) { + driver := &iacServerFakeDriver{ + readOut: &interfaces.ResourceOutput{ + Name: "delegation", + Type: "infra.dns_delegation", + ProviderID: "gocodealone.tech", + Outputs: map[string]any{"nameservers": []any{"ns1.digitalocean.com"}}, + Status: "active", + }, + diff: &interfaces.DiffResult{NeedsUpdate: false}, + } + srv := &hoverIaCServer{ + provider: &HoverProvider{drivers: map[string]interfaces.ResourceDriver{ + "infra.dns_delegation": driver, + }}, + } + + readResp, err := srv.Read(context.Background(), &pb.ResourceReadRequest{ + ResourceType: "infra.dns_delegation", + Ref: &pb.ResourceRef{Name: "delegation", Type: "infra.dns_delegation", ProviderId: "gocodealone.tech"}, + }) + if err != nil { + t.Fatalf("Read: %v", err) + } + if readResp.GetOutput().GetProviderId() != "gocodealone.tech" { + t.Fatalf("Read ProviderID = %q, want gocodealone.tech", readResp.GetOutput().GetProviderId()) + } + if driver.readRef.ProviderID != "gocodealone.tech" { + t.Fatalf("driver read ref = %+v, want provider id gocodealone.tech", driver.readRef) + } + + diffResp, err := srv.Diff(context.Background(), &pb.ResourceDiffRequest{ + ResourceType: "infra.dns_delegation", + Desired: &pb.ResourceSpec{ + Name: "delegation", + Type: "infra.dns_delegation", + ConfigJson: []byte(`{"domain":"gocodealone.tech"}`), + }, + Current: readResp.GetOutput(), + }) + if err != nil { + t.Fatalf("Diff: %v", err) + } + if diffResp.GetResult().GetNeedsUpdate() { + t.Fatal("Diff NeedsUpdate = true, want false") + } + if driver.diffDesired.Config["domain"] != "gocodealone.tech" { + t.Fatalf("driver diff desired config = %+v", driver.diffDesired.Config) + } +} + func TestHoverIaCServer_Destroy_EmptyRefs(t *testing.T) { srv := NewIaCServer() // Destroy with zero refs is a no-op regardless of initialization state. @@ -117,3 +169,42 @@ func TestHoverIaCServer_Destroy_EmptyRefs(t *testing.T) { t.Errorf("expected no destroyed, got %v", resp.GetResult().GetDestroyed()) } } + +type iacServerFakeDriver struct { + readOut *interfaces.ResourceOutput + readRef interfaces.ResourceRef + diff *interfaces.DiffResult + diffDesired interfaces.ResourceSpec + diffCurrent *interfaces.ResourceOutput +} + +func (d *iacServerFakeDriver) Create(_ context.Context, spec interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) { + return &interfaces.ResourceOutput{Name: spec.Name, Type: spec.Type, ProviderID: spec.Name}, nil +} + +func (d *iacServerFakeDriver) Read(_ context.Context, ref interfaces.ResourceRef) (*interfaces.ResourceOutput, error) { + d.readRef = ref + return d.readOut, nil +} + +func (d *iacServerFakeDriver) Update(_ context.Context, ref interfaces.ResourceRef, spec interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) { + return &interfaces.ResourceOutput{Name: spec.Name, Type: spec.Type, ProviderID: ref.ProviderID}, nil +} + +func (d *iacServerFakeDriver) Delete(_ context.Context, _ interfaces.ResourceRef) error { return nil } + +func (d *iacServerFakeDriver) Diff(_ context.Context, desired interfaces.ResourceSpec, current *interfaces.ResourceOutput) (*interfaces.DiffResult, error) { + d.diffDesired = desired + d.diffCurrent = current + return d.diff, nil +} + +func (d *iacServerFakeDriver) HealthCheck(_ context.Context, _ interfaces.ResourceRef) (*interfaces.HealthResult, error) { + return &interfaces.HealthResult{Healthy: true}, nil +} + +func (d *iacServerFakeDriver) Scale(_ context.Context, ref interfaces.ResourceRef, _ int) (*interfaces.ResourceOutput, error) { + return &interfaces.ResourceOutput{Name: ref.Name, Type: ref.Type, ProviderID: ref.ProviderID}, nil +} + +func (d *iacServerFakeDriver) SensitiveKeys() []string { return nil }