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
86 changes: 86 additions & 0 deletions api/v1alpha1/group_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package v1alpha1

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

vault "github.com/hashicorp/vault/api"
vaultutils "github.com/redhat-cop/vault-config-operator/api/v1alpha1/utils"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -217,3 +223,83 @@ func TestGroupConditions(t *testing.T) {
t.Error("expected Group conditions to be set and retrieved")
}
}

// newGroupVaultTestClient creates a vault.Client pointed at the given httptest.Server.
func newGroupVaultTestClient(t *testing.T, srv *httptest.Server) *vault.Client {
t.Helper()
cfg := vault.DefaultConfig()
cfg.Address = srv.URL
client, err := vault.NewClient(cfg)
if err != nil {
t.Fatalf("vault.NewClient: %v", err)
}
client.SetToken("test-token")
return client
}

func TestGroupEnrichStatus(t *testing.T) {
const wantID = "abc-123-def-456"

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"id": wantID,
"name": "my-group",
},
})
}))
defer srv.Close()

group := &Group{
ObjectMeta: metav1.ObjectMeta{Name: "my-group"},
}
ctx := vaultutils.ContextWithVaultClient(context.Background(), newGroupVaultTestClient(t, srv))

if err := group.EnrichStatus(ctx); err != nil {
t.Fatalf("EnrichStatus() unexpected error: %v", err)
}
if group.Status.ID != wantID {
t.Errorf("Status.ID = %q, want %q", group.Status.ID, wantID)
}
}

func TestGroupEnrichStatusNoIDField(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"name": "my-group", // no "id" key
},
})
}))
defer srv.Close()

group := &Group{
ObjectMeta: metav1.ObjectMeta{Name: "my-group"},
}
ctx := vaultutils.ContextWithVaultClient(context.Background(), newGroupVaultTestClient(t, srv))

if err := group.EnrichStatus(ctx); err != nil {
t.Fatalf("EnrichStatus() unexpected error: %v", err)
}
if group.Status.ID != "" {
t.Errorf("expected Status.ID to remain empty, got %q", group.Status.ID)
}
}

func TestGroupEnrichStatusVaultError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, `{"errors":["internal server error"]}`, http.StatusInternalServerError)
}))
defer srv.Close()

group := &Group{
ObjectMeta: metav1.ObjectMeta{Name: "my-group"},
}
ctx := vaultutils.ContextWithVaultClient(context.Background(), newGroupVaultTestClient(t, srv))

if err := group.EnrichStatus(ctx); err == nil {
t.Error("expected error from Vault 500 response, got nil")
}
}
20 changes: 20 additions & 0 deletions api/v1alpha1/group_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ type GroupConfig struct {

// GroupStatus defines the observed state of Group
type GroupStatus struct {
// ID is the Vault-assigned unique identifier for this identity group.
// +kubebuilder:validation:Optional
ID string `json:"id,omitempty"`

// +patchMergeKey=type
// +patchStrategy=merge
// +listType=map
Expand Down Expand Up @@ -114,6 +118,7 @@ func init() {

var _ vaultutils.VaultObject = &Group{}
var _ vaultutils.ConditionsAware = &Group{}
var _ vaultutils.VaultStatusEnricher = &Group{}

func (m *Group) GetConditions() []metav1.Condition {
return m.Status.Conditions
Expand Down Expand Up @@ -178,3 +183,18 @@ func (d *Group) IsEquivalentToDesiredState(payload map[string]any) bool {
desiredState := d.Spec.toMap()
return reflect.DeepEqual(desiredState, filterPayloadToDesiredKeys(desiredState, payload))
}

// EnrichStatus reads the group back from Vault and persists the Vault-assigned ID in status.
func (d *Group) EnrichStatus(ctx context.Context) error {
vaultClient := vaultutils.VaultClientFromContext(ctx)
secret, err := vaultClient.Logical().ReadWithContext(ctx, d.GetPath())
if err != nil {
return err
}
if secret != nil && secret.Data != nil {
if id, ok := secret.Data["id"].(string); ok {
d.Status.ID = id
}
}
return nil
}
7 changes: 7 additions & 0 deletions api/v1alpha1/utils/vaultobject.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ type VaultObject interface {
GetVaultConnection() *VaultConnection
}

// VaultStatusEnricher is an optional interface that VaultObjects can implement
// to enrich their Kubernetes status with data read back from Vault after a
// successful create or update operation.
type VaultStatusEnricher interface {
EnrichStatus(ctx context.Context) error
}

type VaultEndpoint struct {
vaultObject VaultObject
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,14 @@ func (r *VaultResource) manageReconcileLogic(context context.Context, instance c
log.Error(err, "unable to create/update vault resource", "instance", instance)
return err
}

// Enrich status with Vault-side data if the object supports it
if enricher, ok := instance.(vaultutils.VaultStatusEnricher); ok {
if enrichErr := enricher.EnrichStatus(context); enrichErr != nil {
log.Error(enrichErr, "unable to enrich status from Vault", "instance", instance)
// Non-fatal: proceed so conditions are still updated
}
}

return nil
}