Skip to content

Commit

Permalink
Merge pull request #142 from cofide/spire-api-interface
Browse files Browse the repository at this point in the history
Refactor SpireHelm to use a SPIREAPI interface
  • Loading branch information
markgoddard authored Jan 30, 2025
2 parents 0c11da2 + a3fdf7d commit f57601e
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 20 deletions.
2 changes: 1 addition & 1 deletion cmd/cofidectl-test-plugin/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func serveDataSource() error {
if err != nil {
return err
}
spireHelm := spirehelm.NewSpireHelm(nil)
spireHelm := spirehelm.NewSpireHelm(nil, nil)

go_plugin.Serve(&go_plugin.ServeConfig{
HandshakeConfig: plugin.HandshakeConfig,
Expand Down
2 changes: 1 addition & 1 deletion pkg/plugin/manager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func (pm *PluginManager) loadProvision(ctx context.Context) (provision.Provision

// Check if an in-process provision implementation has been requested.
if provisionName == SpireHelmProvisionPluginName {
spireHelm := spirehelm.NewSpireHelm(nil)
spireHelm := spirehelm.NewSpireHelm(nil, nil)
if err := spireHelm.Validate(ctx); err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/plugin/manager/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ func TestManager_GetProvision_success(t *testing.T) {
{
name: "defaults",
config: config.Config{Plugins: GetDefaultPlugins()},
want: spirehelm.NewSpireHelm(nil),
want: spirehelm.NewSpireHelm(nil, nil),
},
{
name: "gRPC",
Expand Down
55 changes: 55 additions & 0 deletions pkg/plugin/provision/spirehelm/spireapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2025 Cofide Limited.
// SPDX-License-Identifier: Apache-2.0

package spirehelm

import (
"context"

kubeutil "github.com/cofide/cofidectl/pkg/kube"
"github.com/cofide/cofidectl/pkg/spire"
)

// Type check that SPIREAPIFactoryImpl implements the SPIREAPIFactory interface.
var _ SPIREAPIFactory = &SPIREAPIFactoryImpl{}

// SPIREAPIFactory is an interface that abstracts the construction of SPIREAPI objects.
type SPIREAPIFactory interface {
// Build returns a SPIREAPI.
Build(kubeCfgFile, kubeContext string) (SPIREAPI, error)
}

// SPIREAPI is an interface that abstracts a subset of the SPIRE server API for use by the SpireHelm plugin.
type SPIREAPI interface {
// WaitForServerIP waits for a SPIRE server pod and service to become ready, then returns the external IP of the service.
WaitForServerIP(ctx context.Context) (string, error)

// GetBundle retrieves a SPIFFE bundle for the local trust zone.
GetBundle(ctx context.Context) (string, error)
}

// SPIREAPIFactoryImpl implements the SPIREAPIFactory interface, building a SPIREAPIImpl.
type SPIREAPIFactoryImpl struct{}

func (f *SPIREAPIFactoryImpl) Build(kubeCfgFile, kubeContext string) (SPIREAPI, error) {
client, err := kubeutil.NewKubeClientFromSpecifiedContext(kubeCfgFile, kubeContext)
if err != nil {
return nil, err
}

return &SPIREAPIImpl{client: client}, nil
}

// SPIREAPIImpl implements the SPIREAPI interface using the Kubernetes API to interact with a
// SPIRE server.
type SPIREAPIImpl struct {
client *kubeutil.Client
}

func (s *SPIREAPIImpl) WaitForServerIP(ctx context.Context) (string, error) {
return spire.WaitForServerIP(ctx, s.client)
}

func (s *SPIREAPIImpl) GetBundle(ctx context.Context) (string, error) {
return spire.GetBundle(ctx, s.client)
}
16 changes: 9 additions & 7 deletions pkg/plugin/provision/spirehelm/spirehelm.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@ import (
trust_zone_proto "github.com/cofide/cofide-api-sdk/gen/go/proto/trust_zone/v1alpha1"

"github.com/cofide/cofidectl/internal/pkg/trustzone"
kubeutil "github.com/cofide/cofidectl/pkg/kube"
"github.com/cofide/cofidectl/pkg/plugin/datasource"
"github.com/cofide/cofidectl/pkg/plugin/provision"
"github.com/cofide/cofidectl/pkg/spire"
)

// Control flow and error handling require some care in this package due to the asynchronous nature
Expand All @@ -30,13 +28,17 @@ var _ provision.Provision = &SpireHelm{}
// SpireHelm implements the `Provision` interface by deploying a SPIRE cluster using the SPIRE Helm charts.
type SpireHelm struct {
providerFactory ProviderFactory
spireAPIFactory SPIREAPIFactory
}

func NewSpireHelm(providerFactory ProviderFactory) *SpireHelm {
func NewSpireHelm(providerFactory ProviderFactory, spireAPIFactory SPIREAPIFactory) *SpireHelm {
if providerFactory == nil {
providerFactory = &HelmSPIREProviderFactory{}
}
return &SpireHelm{providerFactory: providerFactory}
if spireAPIFactory == nil {
spireAPIFactory = &SPIREAPIFactoryImpl{}
}
return &SpireHelm{providerFactory: providerFactory, spireAPIFactory: spireAPIFactory}
}

func (h *SpireHelm) Validate(_ context.Context) error {
Expand Down Expand Up @@ -185,13 +187,13 @@ func (h *SpireHelm) GetBundleAndEndpoint(
sb := provision.NewStatusBuilder(trustZone.Name, cluster.GetName())
statusCh <- sb.Ok("Waiting", "Waiting for SPIRE server pod and service")

client, err := kubeutil.NewKubeClientFromSpecifiedContext(kubeCfgFile, cluster.GetKubernetesContext())
spireAPI, err := h.spireAPIFactory.Build(kubeCfgFile, cluster.GetKubernetesContext())
if err != nil {
statusCh <- sb.Error("Waiting", "Failed waiting for SPIRE server pod and service", err)
return err
}

clusterIP, err := spire.WaitForServerIP(ctx, client)
clusterIP, err := spireAPI.WaitForServerIP(ctx)
if err != nil {
statusCh <- sb.Error("Waiting", "Failed waiting for SPIRE server pod and service", err)
return err
Expand All @@ -202,7 +204,7 @@ func (h *SpireHelm) GetBundleAndEndpoint(
trustZone.BundleEndpointUrl = &bundleEndpointUrl

// Obtain the bundle
bundle, err := spire.GetBundle(ctx, client)
bundle, err := spireAPI.GetBundle(ctx)
if err != nil {
statusCh <- sb.Error("Waiting", "Failed obtaining bundle", err)
return err
Expand Down
60 changes: 50 additions & 10 deletions pkg/plugin/provision/spirehelm/spirehelm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package spirehelm

import (
"context"
"errors"
"testing"

attestation_policy_proto "github.com/cofide/cofide-api-sdk/gen/go/proto/attestation_policy/v1alpha1"
Expand All @@ -24,7 +23,8 @@ import (

func TestSpireHelm_Deploy(t *testing.T) {
providerFactory := newFakeHelmSPIREProviderFactory()
spireHelm := NewSpireHelm(providerFactory)
spireAPIFactory := newFakeSPIREAPIFactory()
spireHelm := NewSpireHelm(providerFactory, spireAPIFactory)
ds := newFakeDataSource(t, defaultConfig())

statusCh, err := spireHelm.Deploy(context.Background(), ds, "fake-kube.cfg")
Expand All @@ -41,19 +41,25 @@ func TestSpireHelm_Deploy(t *testing.T) {
provision.StatusOk("Installing", "Installing SPIRE chart for local2 in tz2"),
provision.StatusDone("Installed", "Installation completed for local2 in tz2"),
provision.StatusOk("Waiting", "Waiting for SPIRE server pod and service for local1 in tz1"),
// FIXME: This attempts to create a real Kubernetes client and fails.
provision.StatusError(
"Waiting",
"Failed waiting for SPIRE server pod and service for local1 in tz1",
errors.New("load from file: open fake-kube.cfg: no such file or directory"),
),
provision.StatusDone("Ready", "All SPIRE server pods and services are ready for local1 in tz1"),
provision.StatusOk("Waiting", "Waiting for SPIRE server pod and service for local2 in tz2"),
provision.StatusDone("Ready", "All SPIRE server pods and services are ready for local2 in tz2"),
provision.StatusOk("Configuring", "Applying post-installation configuration for local1 in tz1"),
provision.StatusDone("Configured", "Post-installation configuration completed for local1 in tz1"),
provision.StatusOk("Configuring", "Applying post-installation configuration for local2 in tz2"),
provision.StatusDone("Configured", "Post-installation configuration completed for local2 in tz2"),
provision.StatusOk("Waiting", "Waiting for SPIRE server pod and service for local1 in tz1"),
provision.StatusDone("Ready", "All SPIRE server pods and services are ready for local1 in tz1"),
provision.StatusOk("Waiting", "Waiting for SPIRE server pod and service for local2 in tz2"),
provision.StatusDone("Ready", "All SPIRE server pods and services are ready for local2 in tz2"),
}
assert.EqualExportedValues(t, want, statuses)
}

func TestSpireHelm_Deploy_ExternalServer(t *testing.T) {
providerFactory := newFakeHelmSPIREProviderFactory()
spireHelm := NewSpireHelm(providerFactory)
spireAPIFactory := newFakeSPIREAPIFactory()
spireHelm := NewSpireHelm(providerFactory, spireAPIFactory)

config := &config.Config{
TrustZones: []*trust_zone_proto.TrustZone{
Expand All @@ -76,14 +82,17 @@ func TestSpireHelm_Deploy_ExternalServer(t *testing.T) {
provision.StatusOk("Installing", "Installing SPIRE chart for local5 in tz5"),
provision.StatusDone("Installed", "Installation completed for local5 in tz5"),
provision.StatusDone("Ready", "Skipped waiting for external SPIRE server pod and service for local5 in tz5"),
provision.StatusOk("Configuring", "Applying post-installation configuration for local5 in tz5"),
provision.StatusDone("Configured", "Post-installation configuration completed for local5 in tz5"),
provision.StatusDone("Ready", "Skipped waiting for external SPIRE server pod and service for local5 in tz5"),
}
assert.EqualExportedValues(t, want, statuses)
}

func TestSpireHelm_TearDown(t *testing.T) {
providerFactory := newFakeHelmSPIREProviderFactory()
spireHelm := NewSpireHelm(providerFactory)
spireAPIFactory := newFakeSPIREAPIFactory()
spireHelm := NewSpireHelm(providerFactory, spireAPIFactory)
ds := newFakeDataSource(t, defaultConfig())

statusCh, err := spireHelm.TearDown(context.Background(), ds, "fake-kube.cfg")
Expand Down Expand Up @@ -148,10 +157,16 @@ func (p *fakeHelmSPIREProvider) Execute(statusCh chan<- *provisionpb.Status) err
}

func (p *fakeHelmSPIREProvider) ExecutePostInstallUpgrade(statusCh chan<- *provisionpb.Status) error {
sb := provision.NewStatusBuilder(p.trustZone.Name, p.cluster.GetName())
statusCh <- sb.Ok("Configuring", "Applying post-installation configuration")
statusCh <- sb.Done("Configured", "Post-installation configuration completed")
return nil
}

func (p *fakeHelmSPIREProvider) ExecuteUpgrade(statusCh chan<- *provisionpb.Status) error {
sb := provision.NewStatusBuilder(p.trustZone.Name, p.cluster.GetName())
statusCh <- sb.Ok("Upgrading", "Upgrading SPIRE chart")
statusCh <- sb.Done("Upgraded", "Upgrade completed")
return nil
}

Expand All @@ -166,6 +181,31 @@ func (p *fakeHelmSPIREProvider) CheckIfAlreadyInstalled() (bool, error) {
return false, nil
}

type fakeSPIREAPIFactory struct{}

func newFakeSPIREAPIFactory() SPIREAPIFactory {
return &fakeSPIREAPIFactory{}
}

func (f *fakeSPIREAPIFactory) Build(kubeCfgFile, kubeContext string) (SPIREAPI, error) {
return &fakeSPIREAPI{}, nil
}

type fakeSPIREAPI struct {
ip string
ipErr error
bundle string
bundleErr error
}

func (s *fakeSPIREAPI) WaitForServerIP(ctx context.Context) (string, error) {
return s.ip, s.ipErr
}

func (s *fakeSPIREAPI) GetBundle(ctx context.Context) (string, error) {
return s.bundle, s.bundleErr
}

func newFakeDataSource(t *testing.T, cfg *config.Config) datasource.DataSource {
configLoader, err := config.NewMemoryLoader(cfg)
require.Nil(t, err)
Expand Down

0 comments on commit f57601e

Please sign in to comment.