blob: 30a1f926ea5026c184a1630d67083202bf76996e [file] [log] [blame]
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package implements the [internal/secret] package using Google Cloud
// Storage's Secret Manager service.
package gcpsecret
import (
"context"
"fmt"
secretmanager "cloud.google.com/go/secretmanager/apiv1"
smpb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// SecretDB implements [secret.DB] using the SecretManager in a GCP project.
// The secret names passed to [SecretDB.Get] and [SecretDB.Set] are used
// directly as the SecretManager names, and the values are used directly
// as the Data field of a SecretPayload.
type SecretDB struct {
client *secretmanager.Client
projectID string
}
// NewSecretDB returns a [SecretDB] using the GCP SecretManager of the given project.
func NewSecretDB(ctx context.Context, projectID string) (*SecretDB, error) {
client, err := secretmanager.NewClient(ctx)
if err != nil {
// unreachable unless the GCP SecretManager service is in a bad state
return nil, err
}
return &SecretDB{client: client, projectID: projectID}, nil
}
// Close closes the SecretDB.
func (db *SecretDB) Close() {
if err := db.client.Close(); err != nil {
// unreachable unless the GCP SecretManager service is in a bad state
panic(err)
}
}
// Get implements [secrets.DB.Get].
func (db *SecretDB) Get(name string) (secret string, ok bool) {
ctx := context.TODO()
result, err := db.client.AccessSecretVersion(ctx, &smpb.AccessSecretVersionRequest{
Name: fmt.Sprintf("projects/%s/secrets/%s/versions/latest", db.projectID, name),
})
if err != nil {
return "", false
}
return string(result.Payload.Data), true
}
// Set implements [secrets.DB.Set].
func (db *SecretDB) Set(name, secret string) {
if err := db.set(context.TODO(), name, secret); err != nil {
// unreachable unless the GCP SecretManager service is in a bad state
panic(err)
}
}
func (db *SecretDB) set(ctx context.Context, name, secret string) error {
add := func() error {
_, err := db.client.AddSecretVersion(ctx, &smpb.AddSecretVersionRequest{
Parent: fmt.Sprintf("projects/%s/secrets/%s", db.projectID, name),
Payload: &smpb.SecretPayload{Data: []byte(secret)},
})
return err
}
err := add()
if err == nil || !isNotFound(err) {
return err
}
// Secret not found. Try to create it.
_, err = db.client.CreateSecret(ctx, &smpb.CreateSecretRequest{
Parent: fmt.Sprintf("projects/%s", db.projectID),
SecretId: name,
Secret: &smpb.Secret{Replication: &smpb.Replication{Replication: &smpb.Replication_Automatic_{}}},
})
if err != nil {
return err
}
return add()
}
// isNotFound reports whether an error returned by the Firestore client is a NotFound
// error.
func isNotFound(err error) bool {
return status.Code(err) == codes.NotFound
}