Skip to content

Commit

Permalink
Add Helm provider interface and a spirehelm unit test
Browse files Browse the repository at this point in the history
  • Loading branch information
markgoddard committed Dec 10, 2024
1 parent 67c65dc commit 7e24760
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 12 deletions.
18 changes: 11 additions & 7 deletions pkg/plugin/provision/spirehelm/providerfactory.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,23 @@ var _ ProviderFactory = &HelmSPIREProviderFactory{}
// ProviderFactory is an interface that abstracts the construction of HelmSPIREProvider objects.
type ProviderFactory interface {
// Build returns a HelmSPIREProvider configured with values for an install/upgrade.
Build(ctx context.Context, ds plugin.DataSource, trustZone *trust_zone_proto.TrustZone) (*helm.HelmSPIREProvider, error)
Build(ctx context.Context, ds plugin.DataSource, trustZone *trust_zone_proto.TrustZone, genValues bool) (helm.Provider, error)
}

// HelmSPIREProviderFactory implements the ProviderFactory interface, building a HelmSPIREProvider
// using the default values generator.
type HelmSPIREProviderFactory struct{}

func (f *HelmSPIREProviderFactory) Build(ctx context.Context, ds plugin.DataSource, trustZone *trust_zone_proto.TrustZone) (*helm.HelmSPIREProvider, error) {
generator := helm.NewHelmValuesGenerator(trustZone, ds, nil)
spireValues, err := generator.GenerateValues()
if err != nil {
return nil, err
func (f *HelmSPIREProviderFactory) Build(ctx context.Context, ds plugin.DataSource, trustZone *trust_zone_proto.TrustZone, genValues bool) (helm.Provider, error) {
spireValues := map[string]any{}
var err error
if genValues {
generator := helm.NewHelmValuesGenerator(trustZone, ds, nil)
spireValues, err = generator.GenerateValues()
if err != nil {
return nil, err
}
}
spireCRDsValues := map[string]interface{}{}
spireCRDsValues := map[string]any{}
return helm.NewHelmSPIREProvider(ctx, trustZone, spireValues, spireCRDsValues)
}
9 changes: 4 additions & 5 deletions pkg/plugin/provision/spirehelm/spirehelm.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
kubeutil "github.com/cofide/cofidectl/pkg/kube"
"github.com/cofide/cofidectl/pkg/plugin"
"github.com/cofide/cofidectl/pkg/plugin/provision"
"github.com/cofide/cofidectl/pkg/provider/helm"
"github.com/cofide/cofidectl/pkg/spire"
)

Expand Down Expand Up @@ -119,7 +118,7 @@ func (h *SpireHelm) ListTrustZones(ds plugin.DataSource) ([]*trust_zone_proto.Tr
}

func (h *SpireHelm) AddSPIRERepository(ctx context.Context, statusCh chan<- *provisionpb.Status) error {
prov, err := helm.NewHelmSPIREProvider(ctx, nil, nil, nil)
prov, err := h.providerFactory.Build(ctx, nil, nil, false)
if err != nil {
statusCh <- provision.StatusError("Preparing", "Failed to create Helm SPIRE provider", err)
return err
Expand All @@ -130,7 +129,7 @@ func (h *SpireHelm) AddSPIRERepository(ctx context.Context, statusCh chan<- *pro

func (h *SpireHelm) InstallSPIREStack(ctx context.Context, ds plugin.DataSource, trustZones []*trust_zone_proto.TrustZone, statusCh chan<- *provisionpb.Status) error {
for _, trustZone := range trustZones {
prov, err := h.providerFactory.Build(ctx, ds, trustZone)
prov, err := h.providerFactory.Build(ctx, ds, trustZone, true)
if err != nil {
sb := provision.NewStatusBuilder(trustZone.Name, trustZone.GetKubernetesCluster())
statusCh <- sb.Error("Deploying", "Failed to create Helm SPIRE provider", err)
Expand Down Expand Up @@ -196,7 +195,7 @@ func (h *SpireHelm) GetBundleAndEndpoint(ctx context.Context, statusCh chan<- *p

func (h *SpireHelm) ApplyPostInstallHelmConfig(ctx context.Context, ds plugin.DataSource, trustZones []*trust_zone_proto.TrustZone, statusCh chan<- *provisionpb.Status) error {
for _, trustZone := range trustZones {
prov, err := h.providerFactory.Build(ctx, ds, trustZone)
prov, err := h.providerFactory.Build(ctx, ds, trustZone, true)
if err != nil {
sb := provision.NewStatusBuilder(trustZone.Name, trustZone.GetKubernetesCluster())
statusCh <- sb.Error("Configuring", "Failed to create Helm SPIRE provider", err)
Expand All @@ -213,7 +212,7 @@ func (h *SpireHelm) ApplyPostInstallHelmConfig(ctx context.Context, ds plugin.Da

func (h *SpireHelm) UninstallSPIREStack(ctx context.Context, trustZones []*trust_zone_proto.TrustZone, statusCh chan<- *provisionpb.Status) error {
for _, trustZone := range trustZones {
prov, err := helm.NewHelmSPIREProvider(ctx, trustZone, nil, nil)
prov, err := h.providerFactory.Build(ctx, nil, trustZone, false)
if err != nil {
sb := provision.NewStatusBuilder(trustZone.Name, trustZone.GetKubernetesCluster())
statusCh <- sb.Error("Uninstalling", "Failed to create Helm SPIRE provider", err)
Expand Down
150 changes: 150 additions & 0 deletions pkg/plugin/provision/spirehelm/spirehelm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Copyright 2024 Cofide Limited.
// SPDX-License-Identifier: Apache-2.0

package spirehelm

import (
"context"
"errors"
"testing"

attestation_policy_proto "github.com/cofide/cofide-api-sdk/gen/go/proto/attestation_policy/v1alpha1"
provisionpb "github.com/cofide/cofide-api-sdk/gen/go/proto/provision_plugin/v1alpha1"
trust_zone_proto "github.com/cofide/cofide-api-sdk/gen/go/proto/trust_zone/v1alpha1"
"github.com/cofide/cofidectl/internal/pkg/config"
"github.com/cofide/cofidectl/internal/pkg/test/fixtures"
"github.com/cofide/cofidectl/pkg/plugin"
"github.com/cofide/cofidectl/pkg/plugin/local"
"github.com/cofide/cofidectl/pkg/plugin/provision"
"github.com/cofide/cofidectl/pkg/provider/helm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

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

statusCh, err := spireHelm.Deploy(context.Background(), ds, "fake-kube.cfg")
require.NoError(t, err, err)

statuses := collectStatuses(statusCh)
want := []*provisionpb.Status{
provision.StatusOk("Preparing", "Adding SPIRE Helm repo"),
provision.StatusDone("Prepared", "Added SPIRE Helm repo"),
provision.StatusOk("Installing", "Installing SPIRE CRDs for local1 in tz1"),
provision.StatusOk("Installing", "Installing SPIRE chart for local1 in tz1"),
provision.StatusDone("Installed", "Installation completed for local1 in tz1"),
provision.StatusOk("Installing", "Installing SPIRE CRDs for local2 in tz2"),
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"),
),
}
assert.EqualExportedValues(t, want, statuses)
}

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

statusCh, err := spireHelm.TearDown(context.Background(), ds)
require.NoError(t, err, err)

statuses := collectStatuses(statusCh)
want := []*provisionpb.Status{
provision.StatusOk("Uninstalling", "Uninstalling SPIRE chart for local1 in tz1"),
provision.StatusDone("Uninstalled", "Uninstallation completed for local1 in tz1"),
provision.StatusOk("Uninstalling", "Uninstalling SPIRE chart for local2 in tz2"),
provision.StatusDone("Uninstalled", "Uninstallation completed for local2 in tz2"),
}
assert.EqualExportedValues(t, want, statuses)
}

func collectStatuses(statusCh <-chan *provisionpb.Status) []*provisionpb.Status {
statuses := []*provisionpb.Status{}
for status := range statusCh {
statuses = append(statuses, status)
}
return statuses
}

type fakeHelmSPIREProviderFactory struct{}

func newFakeHelmSPIREProviderFactory() *fakeHelmSPIREProviderFactory {
return &fakeHelmSPIREProviderFactory{}
}

func (f *fakeHelmSPIREProviderFactory) Build(ctx context.Context, ds plugin.DataSource, trustZone *trust_zone_proto.TrustZone, genValues bool) (helm.Provider, error) {
return newFakeHelmSPIREProvider(trustZone), nil
}

// fakeHelmSPIREProvider implements a fake helm.Provider that can be used in testing.
type fakeHelmSPIREProvider struct {
trustZone *trust_zone_proto.TrustZone
}

func newFakeHelmSPIREProvider(trustZone *trust_zone_proto.TrustZone) helm.Provider {
return &fakeHelmSPIREProvider{trustZone: trustZone}
}

func (p *fakeHelmSPIREProvider) AddRepository(statusCh chan<- *provisionpb.Status) error {
statusCh <- provision.StatusOk("Preparing", "Adding SPIRE Helm repo")
statusCh <- provision.StatusDone("Prepared", "Added SPIRE Helm repo")
return nil
}

func (p *fakeHelmSPIREProvider) Execute(statusCh chan<- *provisionpb.Status) error {
sb := provision.NewStatusBuilder(p.trustZone.Name, p.trustZone.GetKubernetesCluster())
statusCh <- sb.Ok("Installing", "Installing SPIRE CRDs")
statusCh <- sb.Ok("Installing", "Installing SPIRE chart")
statusCh <- sb.Done("Installed", "Installation completed")
return nil
}

func (p *fakeHelmSPIREProvider) ExecutePostInstallUpgrade(statusCh chan<- *provisionpb.Status) error {
return nil
}

func (p *fakeHelmSPIREProvider) ExecuteUpgrade(statusCh chan<- *provisionpb.Status) error {
return nil
}

func (p *fakeHelmSPIREProvider) ExecuteUninstall(statusCh chan<- *provisionpb.Status) error {
sb := provision.NewStatusBuilder(p.trustZone.Name, p.trustZone.GetKubernetesCluster())
statusCh <- sb.Ok("Uninstalling", "Uninstalling SPIRE chart")
statusCh <- sb.Done("Uninstalled", "Uninstallation completed")
return nil
}

func (p *fakeHelmSPIREProvider) CheckIfAlreadyInstalled() (bool, error) {
return false, nil
}

func newFakeDataSource(t *testing.T, cfg *config.Config) plugin.DataSource {
configLoader, err := config.NewMemoryLoader(cfg)
require.Nil(t, err)
lds, err := local.NewLocalDataSource(configLoader)
require.Nil(t, err)
return lds
}

func defaultConfig() *config.Config {
return &config.Config{
TrustZones: []*trust_zone_proto.TrustZone{
fixtures.TrustZone("tz1"),
fixtures.TrustZone("tz2"),
},
AttestationPolicies: []*attestation_policy_proto.AttestationPolicy{
fixtures.AttestationPolicy("ap1"),
fixtures.AttestationPolicy("ap2"),
},
}
}
3 changes: 3 additions & 0 deletions pkg/provider/helm/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ const (
SPIRENamespace = "spire"
)

// Type assertion that HelmSPIREProvider implements the Provider interface.
var _ Provider = &HelmSPIREProvider{}

// HelmSPIREProvider implements a Helm-based installer for the Cofide stack. It uses the SPIFFE/SPIRE project's own
// helm-charts-hardened Helm chart to install a SPIRE stack to a given Kubernetes context, making use of the Cofide
// API concepts and abstractions
Expand Down
37 changes: 37 additions & 0 deletions pkg/provider/helm/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2024 Cofide Limited.
// SPDX-License-Identifier: Apache-2.0

package helm

import (
provisionpb "github.com/cofide/cofide-api-sdk/gen/go/proto/provision_plugin/v1alpha1"
)

// Provider is an interface that abstracts a Helm-based workload identity provider.
type Provider interface {
// AddRepository adds the SPIRE Helm repository to the local repositories.yaml.
// The action is performed synchronously and status is streamed through the provided status channel.
// This function should be called once, not per-trust zone.
// The SPIRE Helm repository is added to the local repositories.yaml, locking the repositories.lock
// file while making changes.
AddRepository(statusCh chan<- *provisionpb.Status) error

// Execute installs the SPIRE Helm stack to the selected Kubernetes context.
// The action is performed synchronously and status is streamed through the provided status channel.
Execute(statusCh chan<- *provisionpb.Status) error

// ExecutePostInstallUpgrade upgrades the SPIRE stack to the selected Kubernetes context.
// The action is performed synchronously and status is streamed through the provided status channel.
ExecutePostInstallUpgrade(statusCh chan<- *provisionpb.Status) error

// ExecuteUpgrade upgrades the SPIRE stack to the selected Kubernetes context.
// The action is performed synchronously and status is streamed through the provided status channel.
ExecuteUpgrade(statusCh chan<- *provisionpb.Status) error

// ExecuteUninstall uninstalls the SPIRE stack from the selected Kubernetes context.
// The action is performed synchronously and status is streamed through the provided status channel.
ExecuteUninstall(statusCh chan<- *provisionpb.Status) error

// CheckIfAlreadyInstalled returns true if the SPIRE chart has previously been installed.
CheckIfAlreadyInstalled() (bool, error)
}

0 comments on commit 7e24760

Please sign in to comment.