diff --git a/CHANGELOG.md b/CHANGELOG.md index 22d4c5d..a624d62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,22 @@ formae agent. ### Added +- `GCP::CertificateManager::CertificateMap` — groups the certificates a load + balancer serves, selected per hostname by its entries. A target HTTPS proxy + points at a map rather than a single certificate, which is how one proxy + serves many domains. + +- `GCP::CertificateManager::DnsAuthorization` — proves control of a domain. + Creating one returns a CNAME to publish; only issuing a managed certificate + waits on that record resolving, so the authorization itself is immediate. + +- `GCP::CertificateManager::TrustConfig` — the certificate authorities a load + balancer will accept client certificates from, for mutual TLS. It must carry + at least one trust store or allowlisted certificate; Certificate Manager + rejects an empty one at create. Note that it appends a trailing newline to + every `pemCertificate` it stores, whatever was sent, so a PEM declared + without one drifts on every re-apply. + - `GCP::Logging::LogBucket` — where log entries are actually retained. A sink routes entries into a bucket and a view is a window onto one, so this is what decides how long logs live and where. diff --git a/pkg/resources/certificatemanager/api.go b/pkg/resources/certificatemanager/api.go new file mode 100644 index 0000000..9f06bb6 --- /dev/null +++ b/pkg/resources/certificatemanager/api.go @@ -0,0 +1,139 @@ +// © 2025 Platform Engineering Labs Inc. +// +// SPDX-License-Identifier: FSL-1.1-ALv2 + +// Package certificatemanager implements GCP Certificate Manager resources. +package certificatemanager + +import ( + "fmt" + "strings" + + "github.com/platform-engineering-labs/formae-plugin-gcp/pkg/resources/base" +) + +// CertificateManagerAPI - Certificate Manager v1. Everything is location-scoped +// and create/delete are long-running operations. +var CertificateManagerAPI = base.APIConfig{ + BaseURL: "https://certificatemanager.googleapis.com/v1", + APIVersion: "v1", + PathBuilder: certificateManagerPathBuilder, + Pagination: &base.PaginationConfig{PageSizeParam: "pageSize"}, +} + +// CertificateManagerOperations - asynchronous. create/delete answer with an +// Operation; formae polls Status until it reports done. +var CertificateManagerOperations = base.OperationConfig{ + Synchronous: false, + OperationIDExtractor: extractOperationName, + OperationURLBuilder: func(_ base.PathContext, opID string) string { return opID }, + NativeIDExtractor: extractCertificateManagerNativeID, + OperationStatusChecker: checkOperationStatus, +} + +// CertificateManagerNativeID - the full resource path, in one of two shapes: +// +// projects/{p}/locations/{l}/{collection}/{name} +// projects/{p}/locations/{l}/certificateMaps/{map}/certificateMapEntries/{entry} +var CertificateManagerNativeID = base.NativeIDConfig{ + Format: base.FullPathFormat, + Parser: parseCertificateManagerNativeID, +} + +// certificateManagerLocation is where these resources live. +// +// Certificate maps, DNS authorizations and trust configs are global. Addressing +// them at the target's region answers "Malformed name: ... [Invalid location in +// resource URL path]", so the location is pinned rather than inherited - which +// also keeps create and discovery pointed at the same place, unlike a fixture +// that pins a region the discovery pass then fails to look in. +const certificateManagerLocation = "global" + +// certificateManagerPathBuilder builds +// /projects/{p}/locations/global[/{parentType}/{parent}]/{resourceType}[/{name}]. +func certificateManagerPathBuilder(ctx base.PathContext) string { + path := fmt.Sprintf("/projects/%s/locations/%s", ctx.Project, certificateManagerLocation) + if ctx.ParentType != "" && ctx.ParentResource != "" { + path += fmt.Sprintf("/%s/%s", ctx.ParentType, ctx.ParentResource) + } + path += "/" + ctx.ResourceType + if ctx.ResourceName != "" { + path += "/" + ctx.ResourceName + } + return path +} + +// parseCertificateManagerNativeID restores the context a read needs, including +// the parent of a map entry - without it a read would address the +// location-level collection and 404. +func parseCertificateManagerNativeID(nativeID string) (base.PathContext, error) { + parts := strings.Split(nativeID, "/") + if len(parts) < 6 || parts[0] != "projects" || parts[2] != "locations" { + return base.PathContext{}, fmt.Errorf("invalid certificate manager native ID: %s", nativeID) + } + ctx := base.PathContext{ + Project: parts[1], + // Always "global" for this API; kept from the id so a native ID round + // trips unchanged. + Location: parts[3], + ResourceType: parts[4], + ResourceName: parts[5], + } + switch len(parts) { + case 6: + case 8: + ctx.ParentType = parts[4] + ctx.ParentResource = parts[5] + ctx.ResourceType = parts[6] + ctx.ResourceName = parts[7] + default: + return base.PathContext{}, fmt.Errorf("invalid certificate manager native ID: %s", nativeID) + } + return ctx, nil +} + +// extractOperationName returns the LRO name from a create or delete response. +func extractOperationName(response map[string]interface{}) string { + if name, ok := response["name"].(string); ok && strings.Contains(name, "/operations/") { + return name + } + return "" +} + +// extractCertificateManagerNativeID builds the resource path. On an async +// create the response is an Operation rather than the resource, so the path +// comes from the context buildPathContext already filled in; a read or a list +// item reports its own full path. +func extractCertificateManagerNativeID(response map[string]interface{}, ctx base.PathContext) string { + if name, ok := response["name"].(string); ok && !strings.Contains(name, "/operations/") { + if i := strings.Index(name, "projects/"); i >= 0 { + return name[i:] + } + } + if ctx.ResourceName == "" { + return "" + } + parent := "" + if ctx.ParentType != "" && ctx.ParentResource != "" { + parent = fmt.Sprintf("%s/%s/", ctx.ParentType, ctx.ParentResource) + } + return fmt.Sprintf("projects/%s/locations/%s/%s%s/%s", + ctx.Project, certificateManagerLocation, parent, ctx.ResourceType, ctx.ResourceName) +} + +// checkOperationStatus reports whether a polled Operation is done, mapping a +// present "error" to a terminal failure. +func checkOperationStatus(op map[string]interface{}) (bool, error) { + done, _ := op["done"].(bool) + if !done { + return false, nil + } + if errObj, ok := op["error"].(map[string]interface{}); ok { + msg, _ := errObj["message"].(string) + if msg == "" { + msg = "operation failed" + } + return true, fmt.Errorf("%s", msg) + } + return true, nil +} diff --git a/pkg/resources/certificatemanager/resources.go b/pkg/resources/certificatemanager/resources.go new file mode 100644 index 0000000..cd3439e --- /dev/null +++ b/pkg/resources/certificatemanager/resources.go @@ -0,0 +1,73 @@ +// © 2025 Platform Engineering Labs Inc. +// +// SPDX-License-Identifier: FSL-1.1-ALv2 + +package certificatemanager + +import ( + "github.com/platform-engineering-labs/formae-plugin-gcp/pkg/resources/base" +) + +const ( + CertificateMapResourceType = "GCP::CertificateManager::CertificateMap" + DnsAuthorizationResourceType = "GCP::CertificateManager::DnsAuthorization" + TrustConfigResourceType = "GCP::CertificateManager::TrustConfig" +) + +var certificateManagerRegistry *base.ResourceRegistry + +func init() { + certificateManagerRegistry = base.NewResourceRegistry( + CertificateManagerAPI, CertificateManagerOperations, CertificateManagerNativeID) + + // All three are global, take their id as a create-time query parameter, and + // patch with a query-string field mask - so the generic engine covers them + // without a custom provisioner. They carry no Scope: ScopeLocationBased + // would make List return nothing whenever the target declares no location, + // and the path builder pins "global" regardless. + err := certificateManagerRegistry.RegisterAll([]base.ResourceDefinition{ + { + // A certificate map groups the certificates a load balancer serves, + // selected per hostname by its entries. + ResourceType: CertificateMapResourceType, + ResourceConfig: base.ResourceConfig{ + ResourceType: "certificateMaps", + CreateIDParam: "certificateMapId", + SupportsUpdate: true, + UpdateMaskFromBody: true, + }, + RequestTransformer: base.DropFieldsOnUpdate("name"), + ResponseTransformer: base.ShortNameResponseTransformer, + }, + { + // A DNS authorization is how Certificate Manager proves control of + // a domain: it hands back a CNAME to publish, and a managed + // certificate for that domain cannot be issued without one. + ResourceType: DnsAuthorizationResourceType, + ResourceConfig: base.ResourceConfig{ + ResourceType: "dnsAuthorizations", + CreateIDParam: "dnsAuthorizationId", + SupportsUpdate: true, + UpdateMaskFromBody: true, + }, + RequestTransformer: base.DropFieldsOnUpdate("name", "domain"), + ResponseTransformer: base.ShortNameResponseTransformer, + }, + { + // A trust config holds the CAs a load balancer will accept client + // certificates from - the anchor set for mutual TLS. + ResourceType: TrustConfigResourceType, + ResourceConfig: base.ResourceConfig{ + ResourceType: "trustConfigs", + CreateIDParam: "trustConfigId", + SupportsUpdate: true, + UpdateMaskFromBody: true, + }, + RequestTransformer: base.DropFieldsOnUpdate("name"), + ResponseTransformer: base.ShortNameResponseTransformer, + }, + }) + if err != nil { + panic(err) + } +} diff --git a/pkg/resources/cfres.go b/pkg/resources/cfres.go index 16957ac..fe268aa 100644 --- a/pkg/resources/cfres.go +++ b/pkg/resources/cfres.go @@ -11,6 +11,7 @@ import ( _ "github.com/platform-engineering-labs/formae-plugin-gcp/pkg/resources/bigquery" _ "github.com/platform-engineering-labs/formae-plugin-gcp/pkg/resources/bigtable" _ "github.com/platform-engineering-labs/formae-plugin-gcp/pkg/resources/certificateauthority" + _ "github.com/platform-engineering-labs/formae-plugin-gcp/pkg/resources/certificatemanager" _ "github.com/platform-engineering-labs/formae-plugin-gcp/pkg/resources/cloudrun" _ "github.com/platform-engineering-labs/formae-plugin-gcp/pkg/resources/cloudscheduler" _ "github.com/platform-engineering-labs/formae-plugin-gcp/pkg/resources/cloudtasks" diff --git a/schema/pkl/certificatemanager/certificateMap.pkl b/schema/pkl/certificatemanager/certificateMap.pkl new file mode 100644 index 0000000..a5c084a --- /dev/null +++ b/schema/pkl/certificatemanager/certificateMap.pkl @@ -0,0 +1,48 @@ +/* + * © 2025 Platform Engineering Labs Inc. + * + * SPDX-License-Identifier: FSL-1.1-ALv2 + */ + +/// GCP Certificate Manager Certificate Map +/// +/// Groups the certificates a load balancer serves, picking one per hostname +/// through its entries. A target HTTPS proxy points at a map instead of at a +/// single certificate, which is how one proxy serves many domains. +module gcp.certificatemanager.certificateMap + +import "../gcp.pkl" +import "@formae/formae.pkl" + +const type = "GCP::CertificateManager::CertificateMap" + +open class CertificateMapResolvable extends formae.Resolvable { + hidden type = module.type + + hidden name: CertificateMapResolvable = (this) { + property = "name" + } +} + +@gcp.ResourceHint { + type = module.type + identifier = "name" +} +open class CertificateMap extends formae.Resource { + hidden parent = this + + /// Short map id. Immutable. + @gcp.FieldHint { createOnly = true } + name: String + + @gcp.FieldHint + description: String? + + @gcp.FieldHint + labels: Mapping? + + hidden res: CertificateMapResolvable = new { + label = parent.label + stack = parent.stack?.label + } +} diff --git a/schema/pkl/certificatemanager/dnsAuthorization.pkl b/schema/pkl/certificatemanager/dnsAuthorization.pkl new file mode 100644 index 0000000..a331f95 --- /dev/null +++ b/schema/pkl/certificatemanager/dnsAuthorization.pkl @@ -0,0 +1,65 @@ +/* + * © 2025 Platform Engineering Labs Inc. + * + * SPDX-License-Identifier: FSL-1.1-ALv2 + */ + +/// GCP Certificate Manager DNS Authorization +/// +/// Proves control of a domain. Creating one returns a CNAME record to publish +/// in that domain's zone; a Google-managed certificate for the domain cannot be +/// issued until the record resolves. The authorization itself costs nothing and +/// is created immediately — only issuance waits on DNS. +module gcp.certificatemanager.dnsAuthorization + +import "../gcp.pkl" +import "@formae/formae.pkl" + +const type = "GCP::CertificateManager::DnsAuthorization" + +/// PER_PROJECT_RECORD authorizes one domain; FIXED_RECORD is the legacy form. +typealias DnsAuthorizationType = "PER_PROJECT_RECORD"|"FIXED_RECORD" + +open class DnsAuthorizationResolvable extends formae.Resolvable { + hidden type = module.type + + hidden name: DnsAuthorizationResolvable = (this) { + property = "name" + } +} + +@gcp.ResourceHint { + type = module.type + identifier = "name" +} +open class DnsAuthorization extends formae.Resource { + hidden parent = this + + /// Short authorization id. Immutable. + @gcp.FieldHint { createOnly = true } + name: String + + /// The domain being authorized, without a trailing dot + /// (e.g. "example.com"). Immutable — an authorization is bound to the + /// domain it was created for. + @gcp.FieldHint { createOnly = true } + domain: String + + /// GCP defaults to PER_PROJECT_RECORD. + @gcp.FieldHint { + createOnly = true + hasProviderDefault = true + } + type: DnsAuthorizationType? + + @gcp.FieldHint + description: String? + + @gcp.FieldHint + labels: Mapping? + + hidden res: DnsAuthorizationResolvable = new { + label = parent.label + stack = parent.stack?.label + } +} diff --git a/schema/pkl/certificatemanager/trustConfig.pkl b/schema/pkl/certificatemanager/trustConfig.pkl new file mode 100644 index 0000000..6a461aa --- /dev/null +++ b/schema/pkl/certificatemanager/trustConfig.pkl @@ -0,0 +1,73 @@ +/* + * © 2025 Platform Engineering Labs Inc. + * + * SPDX-License-Identifier: FSL-1.1-ALv2 + */ + +/// GCP Certificate Manager Trust Config +/// +/// The set of certificate authorities a load balancer will accept client +/// certificates from — the anchor set for mutual TLS. Without one a load +/// balancer has nothing to validate a client certificate against. +module gcp.certificatemanager.trustConfig + +import "../gcp.pkl" +import "@formae/formae.pkl" + +const type = "GCP::CertificateManager::TrustConfig" + +/// A PEM-encoded certificate, used as a trust anchor or an intermediate. +@gcp.SubResourceHint +open class TrustCertificate extends formae.SubResource { + @gcp.FieldHint + pemCertificate: String +} + +/// One store: the roots to trust, plus any intermediates needed to chain to +/// them. +@gcp.SubResourceHint +open class TrustStore extends formae.SubResource { + @gcp.FieldHint + trustAnchors: Listing? + + @gcp.FieldHint + intermediateCas: Listing? +} + +open class TrustConfigResolvable extends formae.Resolvable { + hidden type = module.type + + hidden name: TrustConfigResolvable = (this) { + property = "name" + } +} + +@gcp.ResourceHint { + type = module.type + identifier = "name" +} +open class TrustConfig extends formae.Resource { + hidden parent = this + + /// Short config id. Immutable. + @gcp.FieldHint { createOnly = true } + name: String + + @gcp.FieldHint + description: String? + + /// The trust stores this config offers. Certificate Manager requires at + /// least one trust store or allowlisted certificate: a config carrying + /// neither is rejected at create with "trust config must contain at least + /// one trust store or allowlisted certificate". + @gcp.FieldHint + trustStores: Listing? + + @gcp.FieldHint + labels: Mapping? + + hidden res: TrustConfigResolvable = new { + label = parent.label + stack = parent.stack?.label + } +} diff --git a/testdata/certmanager-certificate-map-update.pkl b/testdata/certmanager-certificate-map-update.pkl new file mode 100644 index 0000000..f685d0f --- /dev/null +++ b/testdata/certmanager-certificate-map-update.pkl @@ -0,0 +1,27 @@ +/* + * © 2025 Platform Engineering Labs Inc. + * + * SPDX-License-Identifier: FSL-1.1-ALv2 + */ + +amends "@formae/forma.pkl" + +import "@gcp/certificatemanager/certificateMap.pkl" as certificateMap +import "./config/vars.pkl" as v + +// A map with no entries is valid and serves nothing, which keeps this case free +// of prerequisites. +forma { + v.stack + v.target + + new certificateMap.CertificateMap { + label = "plugin-sdk-test-certmanager-map" + name = "formae-test-certmap-\(v.testRunID)" + description = "Updated certificate map description" + labels = new Mapping { + ["environment"] = "updated" + ["managed-by"] = "formae" + } + } +} diff --git a/testdata/certmanager-certificate-map.pkl b/testdata/certmanager-certificate-map.pkl new file mode 100644 index 0000000..d05ff0a --- /dev/null +++ b/testdata/certmanager-certificate-map.pkl @@ -0,0 +1,27 @@ +/* + * © 2025 Platform Engineering Labs Inc. + * + * SPDX-License-Identifier: FSL-1.1-ALv2 + */ + +amends "@formae/forma.pkl" + +import "@gcp/certificatemanager/certificateMap.pkl" as certificateMap +import "./config/vars.pkl" as v + +// A map with no entries is valid and serves nothing, which keeps this case free +// of prerequisites. +forma { + v.stack + v.target + + new certificateMap.CertificateMap { + label = "plugin-sdk-test-certmanager-map" + name = "formae-test-certmap-\(v.testRunID)" + description = "Test certificate map for plugin SDK conformance tests" + labels = new Mapping { + ["environment"] = "test" + ["managed-by"] = "formae" + } + } +} diff --git a/testdata/certmanager-dns-authorization-update.pkl b/testdata/certmanager-dns-authorization-update.pkl new file mode 100644 index 0000000..d83efb2 --- /dev/null +++ b/testdata/certmanager-dns-authorization-update.pkl @@ -0,0 +1,25 @@ +/* + * © 2025 Platform Engineering Labs Inc. + * + * SPDX-License-Identifier: FSL-1.1-ALv2 + */ + +amends "@formae/forma.pkl" + +import "@gcp/certificatemanager/dnsAuthorization.pkl" as dnsAuthorization +import "./config/vars.pkl" as v + +// Creating the authorization is immediate and free - it just hands back a CNAME +// to publish. Only issuing a certificate against it waits on DNS, and this case +// does not issue one, so example.com never has to resolve. +forma { + v.stack + v.target + + new dnsAuthorization.DnsAuthorization { + label = "plugin-sdk-test-certmanager-dns-auth" + name = "formae-test-dnsauth-\(v.testRunID)" + domain = "formae-\(v.testRunID).example.com" + description = "Updated DNS authorization description" + } +} diff --git a/testdata/certmanager-dns-authorization.pkl b/testdata/certmanager-dns-authorization.pkl new file mode 100644 index 0000000..833372b --- /dev/null +++ b/testdata/certmanager-dns-authorization.pkl @@ -0,0 +1,25 @@ +/* + * © 2025 Platform Engineering Labs Inc. + * + * SPDX-License-Identifier: FSL-1.1-ALv2 + */ + +amends "@formae/forma.pkl" + +import "@gcp/certificatemanager/dnsAuthorization.pkl" as dnsAuthorization +import "./config/vars.pkl" as v + +// Creating the authorization is immediate and free - it just hands back a CNAME +// to publish. Only issuing a certificate against it waits on DNS, and this case +// does not issue one, so example.com never has to resolve. +forma { + v.stack + v.target + + new dnsAuthorization.DnsAuthorization { + label = "plugin-sdk-test-certmanager-dns-auth" + name = "formae-test-dnsauth-\(v.testRunID)" + domain = "formae-\(v.testRunID).example.com" + description = "Test DNS authorization for plugin SDK conformance tests" + } +} diff --git a/testdata/certmanager-trust-config-update.pkl b/testdata/certmanager-trust-config-update.pkl new file mode 100644 index 0000000..4b4888e --- /dev/null +++ b/testdata/certmanager-trust-config-update.pkl @@ -0,0 +1,65 @@ +/* + * © 2025 Platform Engineering Labs Inc. + * + * SPDX-License-Identifier: FSL-1.1-ALv2 + */ + +amends "@formae/forma.pkl" + +import "@gcp/certificatemanager/trustConfig.pkl" as trustConfig +import "./config/vars.pkl" as v + +// Certificate Manager rejects a trust config that carries neither a trust store +// nor an allowlisted certificate: "trust config must contain at least one trust +// store or allowlisted certificate". So the fixture pins one anchor. The PEM is +// a throwaway self-signed certificate generated for this test - it anchors +// nothing real and holds no private key. +// +// The blank line before the closing delimiter is load-bearing: Certificate +// Manager appends a trailing newline to every pemCertificate it stores, +// whatever was sent, so a PEM declared without one never matches on read and +// the resource drifts on every re-apply. +forma { + v.stack + v.target + + new trustConfig.TrustConfig { + label = "plugin-sdk-test-certmanager-trust-config" + name = "formae-test-trustcfg-\(v.testRunID)" + description = "Updated trust config description" + trustStores = new Listing { + new trustConfig.TrustStore { + trustAnchors = new Listing { + new trustConfig.TrustCertificate { + pemCertificate = """ + -----BEGIN CERTIFICATE----- + MIIDMzCCAhugAwIBAgIUZJqm2ZH6GOG2DZ5+vLMJNFLeM3kwDQYJKoZIhvcNAQEL + BQAwKTEnMCUGA1UEAwweZm9ybWFlLWNvbmZvcm1hbmNlLXRlc3QtYW5jaG9yMB4X + DTI2MDgzMTE0NDk0NloXDTQ2MDgyNjE0NDk0NlowKTEnMCUGA1UEAwweZm9ybWFl + LWNvbmZvcm1hbmNlLXRlc3QtYW5jaG9yMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A + MIIBCgKCAQEAyWzZ1iBZLFs+vSTC8e8DDv3+Gh80PzPRP3Gyl1L9dMAET2nZQyfF + uviJ/t9cBbHOoswDP1uYOXUghxADEOx9FUsWdCfKL8gZfTNQxX77mNgHKdDQ0x1c + gpF4ZbI2q7aZ/wNq+2dM2M3yvO79sTWzc+gyGXA3ukyZqvjToUwg87rb24QZ1uAZ + uRPfoFti1jQXRUvUtNCBjhW0SQITc9esN2k/JqJtu98Kngphyd3rUP3J7b+rZrDn + 1DtWWNsS+KHOOz41OlXvZnnu8yJ59VfRoRA+eSHuv+tmK/di/YaL32m8QoStDpCy + 36GLtzX5FPWf9su3NewhQUGMgs3EwwY29wIDAQABo1MwUTAdBgNVHQ4EFgQU58iv + 5+wlBZNnLehoe4JxpSSUGX4wHwYDVR0jBBgwFoAU58iv5+wlBZNnLehoe4JxpSSU + GX4wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAR+0Ducak3BYk + bFeNj1Jnutt1MSJrnp/LkN1hFHbADoLu7aiw/WIwQ0mJcyTq16MRZnqiZl+77i1v + 8uqIY/SaGS0AAYW4UgbBg8sB8V4Oo2guxIDrCsG176DrgyofE2yQlMtfOF7YHw7P + bS4826epY5CnDjkhFHF406dZcPQVTsFUGFRfBkC1xDhGKgta1JVvrc63NdAvpUxT + 5yNT1KIJUoNKzQE9HV0oNPpc5RiwWZs6BRBiQ0tj8HbiHnxkTfLLFymApLoHamgb + LAh362zDAnzZ32PDrLn5fX/nCo1QV/1/l+9XaSoCLB/mC7dDQRPEn65sSmnxUAm8 + lAUYL4cUKA== + -----END CERTIFICATE----- + + """ + } + } + } + } + labels = new Mapping { + ["environment"] = "test" + } + } +} diff --git a/testdata/certmanager-trust-config.pkl b/testdata/certmanager-trust-config.pkl new file mode 100644 index 0000000..694623f --- /dev/null +++ b/testdata/certmanager-trust-config.pkl @@ -0,0 +1,65 @@ +/* + * © 2025 Platform Engineering Labs Inc. + * + * SPDX-License-Identifier: FSL-1.1-ALv2 + */ + +amends "@formae/forma.pkl" + +import "@gcp/certificatemanager/trustConfig.pkl" as trustConfig +import "./config/vars.pkl" as v + +// Certificate Manager rejects a trust config that carries neither a trust store +// nor an allowlisted certificate: "trust config must contain at least one trust +// store or allowlisted certificate". So the fixture pins one anchor. The PEM is +// a throwaway self-signed certificate generated for this test - it anchors +// nothing real and holds no private key. +// +// The blank line before the closing delimiter is load-bearing: Certificate +// Manager appends a trailing newline to every pemCertificate it stores, +// whatever was sent, so a PEM declared without one never matches on read and +// the resource drifts on every re-apply. +forma { + v.stack + v.target + + new trustConfig.TrustConfig { + label = "plugin-sdk-test-certmanager-trust-config" + name = "formae-test-trustcfg-\(v.testRunID)" + description = "Test trust config for plugin SDK conformance tests" + trustStores = new Listing { + new trustConfig.TrustStore { + trustAnchors = new Listing { + new trustConfig.TrustCertificate { + pemCertificate = """ + -----BEGIN CERTIFICATE----- + MIIDMzCCAhugAwIBAgIUZJqm2ZH6GOG2DZ5+vLMJNFLeM3kwDQYJKoZIhvcNAQEL + BQAwKTEnMCUGA1UEAwweZm9ybWFlLWNvbmZvcm1hbmNlLXRlc3QtYW5jaG9yMB4X + DTI2MDgzMTE0NDk0NloXDTQ2MDgyNjE0NDk0NlowKTEnMCUGA1UEAwweZm9ybWFl + LWNvbmZvcm1hbmNlLXRlc3QtYW5jaG9yMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A + MIIBCgKCAQEAyWzZ1iBZLFs+vSTC8e8DDv3+Gh80PzPRP3Gyl1L9dMAET2nZQyfF + uviJ/t9cBbHOoswDP1uYOXUghxADEOx9FUsWdCfKL8gZfTNQxX77mNgHKdDQ0x1c + gpF4ZbI2q7aZ/wNq+2dM2M3yvO79sTWzc+gyGXA3ukyZqvjToUwg87rb24QZ1uAZ + uRPfoFti1jQXRUvUtNCBjhW0SQITc9esN2k/JqJtu98Kngphyd3rUP3J7b+rZrDn + 1DtWWNsS+KHOOz41OlXvZnnu8yJ59VfRoRA+eSHuv+tmK/di/YaL32m8QoStDpCy + 36GLtzX5FPWf9su3NewhQUGMgs3EwwY29wIDAQABo1MwUTAdBgNVHQ4EFgQU58iv + 5+wlBZNnLehoe4JxpSSUGX4wHwYDVR0jBBgwFoAU58iv5+wlBZNnLehoe4JxpSSU + GX4wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAR+0Ducak3BYk + bFeNj1Jnutt1MSJrnp/LkN1hFHbADoLu7aiw/WIwQ0mJcyTq16MRZnqiZl+77i1v + 8uqIY/SaGS0AAYW4UgbBg8sB8V4Oo2guxIDrCsG176DrgyofE2yQlMtfOF7YHw7P + bS4826epY5CnDjkhFHF406dZcPQVTsFUGFRfBkC1xDhGKgta1JVvrc63NdAvpUxT + 5yNT1KIJUoNKzQE9HV0oNPpc5RiwWZs6BRBiQ0tj8HbiHnxkTfLLFymApLoHamgb + LAh362zDAnzZ32PDrLn5fX/nCo1QV/1/l+9XaSoCLB/mC7dDQRPEn65sSmnxUAm8 + lAUYL4cUKA== + -----END CERTIFICATE----- + + """ + } + } + } + } + labels = new Mapping { + ["environment"] = "test" + } + } +}