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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
139 changes: 139 additions & 0 deletions pkg/resources/certificatemanager/api.go
Original file line number Diff line number Diff line change
@@ -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
}
73 changes: 73 additions & 0 deletions pkg/resources/certificatemanager/resources.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
1 change: 1 addition & 0 deletions pkg/resources/cfres.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
48 changes: 48 additions & 0 deletions schema/pkl/certificatemanager/certificateMap.pkl
Original file line number Diff line number Diff line change
@@ -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<String, String>?

hidden res: CertificateMapResolvable = new {
label = parent.label
stack = parent.stack?.label
}
}
65 changes: 65 additions & 0 deletions schema/pkl/certificatemanager/dnsAuthorization.pkl
Original file line number Diff line number Diff line change
@@ -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<String, String>?

hidden res: DnsAuthorizationResolvable = new {
label = parent.label
stack = parent.stack?.label
}
}
Loading