diff --git a/cmd/export_outputs_test.go b/cmd/export_outputs_test.go index b340390..3522e29 100644 --- a/cmd/export_outputs_test.go +++ b/cmd/export_outputs_test.go @@ -16,12 +16,12 @@ import ( func TestParseOutputPath(t *testing.T) { tests := []struct { - name string - raw string - wantOk bool - wantResType string - wantLabel string - wantAttrPath string + name string + raw string + wantOk bool + wantResType string + wantLabel string + wantAttrPath string }{ { name: "valid 3-segment path", diff --git a/definitions/embed.go b/definitions/embed.go index 975ee4c..cf64ec5 100644 --- a/definitions/embed.go +++ b/definitions/embed.go @@ -4,8 +4,10 @@ package definitions import "embed" // FS contains all embedded resource definition YAML files. -// The embed directive includes all .yaml files under pingone subdirectories. -// Subdirectories (base/, davinci/, etc.) are organizational only. +// The embed directive includes all .yaml files under any pingone category +// subdirectory (base/, davinci/, sso/, etc.) — subdirectories are +// organizational only. New category directories do not require editing this +// file. // -//go:embed pingone/base/*.yaml pingone/davinci/*.yaml +//go:embed pingone/*/*.yaml var FS embed.FS diff --git a/go.mod b/go.mod index 19cadee..d5f232a 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,13 @@ module github.com/pingidentity/pingcli-plugin-terraformer -go 1.25.1 +go 1.25.6 require ( github.com/google/uuid v1.6.0 github.com/hashicorp/go-plugin v1.7.0 github.com/hashicorp/hcl/v2 v2.24.0 + github.com/patrickcping/pingone-go-sdk-v2 v0.14.14 + github.com/patrickcping/pingone-go-sdk-v2/management v0.70.0 github.com/pingidentity/pingcli v0.8.0 github.com/pingidentity/pingone-go-client v0.12.0 github.com/spf13/pflag v1.0.10 @@ -35,6 +37,11 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/oklog/run v1.2.0 // indirect + github.com/patrickcping/pingone-go-sdk-v2/authorize v0.8.3 // indirect + github.com/patrickcping/pingone-go-sdk-v2/credentials v0.12.1 // indirect + github.com/patrickcping/pingone-go-sdk-v2/mfa v0.25.1 // indirect + github.com/patrickcping/pingone-go-sdk-v2/risk v0.21.1 // indirect + github.com/patrickcping/pingone-go-sdk-v2/verify v0.11.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/zalando/go-keyring v0.2.8 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect diff --git a/go.sum b/go.sum index 10b09ae..61cd8c1 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,20 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= +github.com/patrickcping/pingone-go-sdk-v2 v0.14.14 h1:illFMogbGgYoZUr+kW0OQjbLyTfGEemLmTcX6oIIdx8= +github.com/patrickcping/pingone-go-sdk-v2 v0.14.14/go.mod h1:DhdLzztyNuH03k4h/AvmLjiYdvxHf1EViZBwDMan6BM= +github.com/patrickcping/pingone-go-sdk-v2/authorize v0.8.3 h1:rB2g0Ry+AlvCxN0gxXNb2AQm/1kyB3G47g2H3KRPHVQ= +github.com/patrickcping/pingone-go-sdk-v2/authorize v0.8.3/go.mod h1:Tb8FQPxveYj4QOc4VJa42QccMv2hSLNpTKDUSdWXtVk= +github.com/patrickcping/pingone-go-sdk-v2/credentials v0.12.1 h1:7g4f1UWz85C4Onwg5hfqZPvyWrbqXJ5HlYNeVDRD8Sk= +github.com/patrickcping/pingone-go-sdk-v2/credentials v0.12.1/go.mod h1:SaqdGPTxYXaeVgOCJXDDCK40Avr+zGujG0YJGQTwyJg= +github.com/patrickcping/pingone-go-sdk-v2/management v0.70.0 h1:O+tBQC6AreSSoFAproARMxaxcPL3hDJKJmb7GkVm7Sg= +github.com/patrickcping/pingone-go-sdk-v2/management v0.70.0/go.mod h1:Ld0egUhvuv9SRvTjTgQrzZur6tB+qBTxm4kSfX6uj6Y= +github.com/patrickcping/pingone-go-sdk-v2/mfa v0.25.1 h1:qLwEQfXJwKsStn935tEUyt9L6r7rJRuukmOfVwRX4H0= +github.com/patrickcping/pingone-go-sdk-v2/mfa v0.25.1/go.mod h1:U5xu9EHj1Wvgl2an+qoq8QgScAPYB02bF0CKQmpo08Y= +github.com/patrickcping/pingone-go-sdk-v2/risk v0.21.1 h1:hjXbUFYkKuQEIyu/m8+yDXOkJRIuIB4/LIy1WZ0Zf+U= +github.com/patrickcping/pingone-go-sdk-v2/risk v0.21.1/go.mod h1:3emXaUBwjjZd6znlR4l9z52j8mryoCuzXOajlQH5Biw= +github.com/patrickcping/pingone-go-sdk-v2/verify v0.11.2 h1:BfJGv6gmHzUDNYI/ZyqvdBIQVPlkAKwnjYs/kevD/PE= +github.com/patrickcping/pingone-go-sdk-v2/verify v0.11.2/go.mod h1:qbJfZ1MXPDeouQBsWelraoNfmguOT1/RBJFuAnQvrSA= github.com/pingidentity/pingcli v0.8.0 h1:KmUuzGliAtNlLz/okMRtT0vdRoiSAz3Kb+/FGti6rpA= github.com/pingidentity/pingcli v0.8.0/go.mod h1:3fAj8w0kZtLU6DBLKdCLQnPCN+gbVjysnwGrgsJoSYY= github.com/pingidentity/pingone-go-client v0.12.0 h1:t9L18Qul3UXqqvi0578R7bzys54cEEl+oSJDT6rMdyM= diff --git a/internal/platform/pingone/client.go b/internal/platform/pingone/client.go index 5d846b2..f966e80 100644 --- a/internal/platform/pingone/client.go +++ b/internal/platform/pingone/client.go @@ -12,6 +12,8 @@ import ( "fmt" "github.com/google/uuid" + "github.com/patrickcping/pingone-go-sdk-v2/management" + pingonesdkv2 "github.com/patrickcping/pingone-go-sdk-v2/pingone" "github.com/pingidentity/pingone-go-client/config" "github.com/pingidentity/pingone-go-client/oauth2" "github.com/pingidentity/pingone-go-client/pingone" @@ -28,9 +30,11 @@ var _ clients.APIClient = (*Client)(nil) // Resource-specific list/get logic and custom handlers live in resource_*.go // files. Each file registers everything for its resource via init(). type Client struct { - apiClient *pingone.APIClient - environmentID uuid.UUID - warnings []string + apiClient *pingone.APIClient + managementCfg *pingonesdkv2.Config + managementClient *management.APIClient + environmentID uuid.UUID + warnings []string } // New creates a DaVinci APIClient from a pre-built SDK client and environment ID. @@ -38,6 +42,34 @@ func New(apiClient *pingone.APIClient, environmentID uuid.UUID) *Client { return &Client{apiClient: apiClient, environmentID: environmentID} } +// NewWithManagementClient creates a Client from a pre-built DaVinci SDK +// client and a pre-built management SDK client. Used by tests that need to +// inject a fake management.APIClient without performing a real OAuth +// exchange. +func NewWithManagementClient(apiClient *pingone.APIClient, managementClient *management.APIClient, environmentID uuid.UUID) *Client { + return &Client{apiClient: apiClient, managementClient: managementClient, environmentID: environmentID} +} + +// management lazily builds and caches the management SDK API client. The +// management SDK performs an OAuth token exchange as soon as the client is +// built (unlike the DaVinci SDK, which defers auth to the first request), so +// construction is deferred until a management resource handler actually +// needs it. +func (c *Client) management(ctx context.Context) (*management.APIClient, error) { + if c.managementClient != nil { + return c.managementClient, nil + } + if c.managementCfg == nil { + return nil, fmt.Errorf("management API client not configured") + } + client, err := c.managementCfg.ManagementAPIClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to initialize management API client: %w", err) + } + c.managementClient = client + return client, nil +} + // Platform returns the platform identifier. func (c *Client) Platform() string { return "pingone" } @@ -106,12 +138,20 @@ func NewFromCredentials(ctx context.Context, workerEnvID, exportEnvID, region, c return nil, fmt.Errorf("failed to initialize API client: %w", err) } + regionCode := management.EnumRegionCode(region) + managementCfg := &pingonesdkv2.Config{ + ClientID: &clientID, + ClientSecret: &clientSecret, + EnvironmentID: &workerEnvID, + RegionCode: ®ionCode, + } + envUUID, err := uuid.Parse(exportEnvID) if err != nil { return nil, fmt.Errorf("invalid export environment ID format: %w", err) } - return &Client{apiClient: apiClient, environmentID: envUUID}, nil + return &Client{apiClient: apiClient, managementCfg: managementCfg, environmentID: envUUID}, nil } // ValidRegions returns the list of valid PingOne region codes. diff --git a/internal/platform/pingone/client_test.go b/internal/platform/pingone/client_test.go index b3241cf..6c1fe14 100644 --- a/internal/platform/pingone/client_test.go +++ b/internal/platform/pingone/client_test.go @@ -30,8 +30,12 @@ func TestNewClient(t *testing.T) { func TestNewFromCredentials(t *testing.T) { ctx := context.Background() - // A valid UUID to use as exportEnvID in success cases. + // Valid UUIDs — real PingOne client/environment IDs are UUIDs, and the + // management SDK's Config.Validate() rejects non-UUID-shaped values. validExportEnvID := "00000000-0000-0000-0000-000000000001" + validWorkerEnvID := "00000000-0000-0000-0000-000000000010" + validClientID := "00000000-0000-0000-0000-000000000020" + validClientSecret := "secret-123" tests := []struct { name string @@ -48,96 +52,96 @@ func TestNewFromCredentials(t *testing.T) { workerEnvID: "", exportEnvID: validExportEnvID, region: "NA", - clientID: "client-123", - clientSecret: "secret-123", + clientID: validClientID, + clientSecret: validClientSecret, expectError: true, errorContains: "auth environment ID is required", }, { name: "missing target environment ID", - workerEnvID: "auth-env-123", + workerEnvID: validWorkerEnvID, exportEnvID: "", region: "NA", - clientID: "client-123", - clientSecret: "secret-123", + clientID: validClientID, + clientSecret: validClientSecret, expectError: true, errorContains: "target environment ID is required", }, { name: "missing region", - workerEnvID: "auth-env-123", + workerEnvID: validWorkerEnvID, exportEnvID: validExportEnvID, region: "", - clientID: "client-123", - clientSecret: "secret-123", + clientID: validClientID, + clientSecret: validClientSecret, expectError: true, errorContains: "region is required", }, { name: "invalid region", - workerEnvID: "auth-env-123", + workerEnvID: validWorkerEnvID, exportEnvID: validExportEnvID, region: "XX", - clientID: "client-123", - clientSecret: "secret-123", + clientID: validClientID, + clientSecret: validClientSecret, expectError: true, errorContains: "invalid region: XX", }, { name: "missing client ID", - workerEnvID: "auth-env-123", + workerEnvID: validWorkerEnvID, exportEnvID: validExportEnvID, region: "NA", clientID: "", - clientSecret: "secret-123", + clientSecret: validClientSecret, expectError: true, errorContains: "client ID is required", }, { name: "missing client secret", - workerEnvID: "auth-env-123", + workerEnvID: validWorkerEnvID, exportEnvID: validExportEnvID, region: "NA", - clientID: "client-123", + clientID: validClientID, clientSecret: "", expectError: true, errorContains: "client secret is required", }, { name: "invalid export environment ID format", - workerEnvID: "auth-env-123", + workerEnvID: validWorkerEnvID, exportEnvID: "not-a-uuid", region: "NA", - clientID: "client-123", - clientSecret: "secret-123", + clientID: validClientID, + clientSecret: validClientSecret, expectError: true, errorContains: "invalid export environment ID format", }, { name: "valid credentials", - workerEnvID: "auth-env-123", + workerEnvID: validWorkerEnvID, exportEnvID: validExportEnvID, region: "NA", - clientID: "client-123", - clientSecret: "secret-123", + clientID: validClientID, + clientSecret: validClientSecret, expectError: false, }, { name: "valid credentials AU region", - workerEnvID: "auth-env-123", + workerEnvID: validWorkerEnvID, exportEnvID: "00000000-0000-0000-0000-000000000002", region: "AU", - clientID: "client-123", - clientSecret: "secret-123", + clientID: validClientID, + clientSecret: validClientSecret, expectError: false, }, { name: "valid credentials SG region", - workerEnvID: "auth-env-123", + workerEnvID: validWorkerEnvID, exportEnvID: "00000000-0000-0000-0000-000000000003", region: "SG", - clientID: "client-123", - clientSecret: "secret-123", + clientID: validClientID, + clientSecret: validClientSecret, expectError: false, }, }