diff --git a/cmd/helm-storage-backend/main.go b/cmd/helm-storage-backend/main.go index 3b61714e4..ff3b8d761 100644 --- a/cmd/helm-storage-backend/main.go +++ b/cmd/helm-storage-backend/main.go @@ -74,7 +74,8 @@ func main() { var handler storage_backend.StorageBackendServer switch cfg.Mode { case HelmReleaseMode: - handler = helm_storage_backend.NewReleaseHandler(logger, helmCfgFlags) + handler, err = helm_storage_backend.NewReleaseHandler(logger, helmCfgFlags) + exitOnError(err, "while creating Helm Release backend storage") case HelmTemplateMode: handler = helm_storage_backend.NewTemplateHandler(logger, helmCfgFlags) default: diff --git a/internal/helm-storage-backend/helm.go b/internal/helm-storage-backend/helm.go new file mode 100644 index 000000000..a6d7fec9c --- /dev/null +++ b/internal/helm-storage-backend/helm.go @@ -0,0 +1,36 @@ +package helmstoragebackend + +import ( + "github.com/pkg/errors" + "helm.sh/helm/v3/pkg/action" + "k8s.io/cli-runtime/pkg/genericclioptions" + + "capact.io/capact/internal/ptr" +) + +const defaultHelmDriver = "secrets" + +type actionConfigurationProducerFn func(flags *genericclioptions.ConfigFlags, driver string, ns string) (*action.Configuration, error) + +// ActionConfigurationProducer returns Configuration with a given input settings. +func ActionConfigurationProducer(flags *genericclioptions.ConfigFlags, driver, ns string) (*action.Configuration, error) { + actionConfig := new(action.Configuration) + helmCfg := &genericclioptions.ConfigFlags{ + APIServer: flags.APIServer, + Insecure: flags.Insecure, + CAFile: flags.CAFile, + BearerToken: flags.BearerToken, + Namespace: ptr.String(ns), + } + + debugLog := func(format string, v ...interface{}) { + // noop + } + + err := actionConfig.Init(helmCfg, ns, driver, debugLog) + if err != nil { + return nil, errors.Wrap(err, "while initializing Helm configuration") + } + + return actionConfig, nil +} diff --git a/internal/helm-storage-backend/release.go b/internal/helm-storage-backend/release.go index 3649be15e..1b8e6be35 100644 --- a/internal/helm-storage-backend/release.go +++ b/internal/helm-storage-backend/release.go @@ -2,32 +2,193 @@ package helmstoragebackend import ( "context" + "encoding/json" + "fmt" - pb "capact.io/capact/pkg/hub/api/grpc/storage_backend" + "github.com/pkg/errors" "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/storage/driver" "k8s.io/cli-runtime/pkg/genericclioptions" + + "capact.io/capact/internal/ptr" + pb "capact.io/capact/pkg/hub/api/grpc/storage_backend" ) var _ pb.StorageBackendServer = &ReleaseHandler{} +const latestRevisionIndicator = 0 + +type ( + // ReleaseDetails holds Helm release details. + ReleaseDetails struct { + // Name specifies installed Helm release name. + Name string `json:"name"` + // Namespace specifies in which Kubernetes Namespace Helm release was installed. + Namespace string `json:"namespace"` + // Chart holds Helm Chart details. + Chart ChartDetails `json:"chart"` + } + + // ChartDetails holds Helm chart details. + ChartDetails struct { + // Name specifies Helm Chart name. + Name string `json:"name"` + // Version specifies the exact chart version. + Version string `json:"version"` + // Repo specifies URL where to locate the requested chart. + Repo string `json:"repo"` + } + + // ReleaseContext holds context used by Helm release storage backend. + ReleaseContext struct { + // Name specifies Helm release name for a given request. + Name string `json:"name"` + // Namespace specifies in which Kubernetes Namespace Helm release is located. + Namespace string `json:"namespace"` + // ChartLocation specifies Helm Chart location. + ChartLocation string `json:"chartLocation"` + // Driver specifies drivers used for storing the Helm release. + Driver *string `json:"driver,omitempty"` + } +) + // ReleaseHandler handles incoming requests to the Helm release storage backend gRPC server. type ReleaseHandler struct { pb.UnimplementedStorageBackendServer - log *zap.Logger + log *zap.Logger + helmCfgFlags *genericclioptions.ConfigFlags + actionConfigurationProducer actionConfigurationProducerFn } // NewReleaseHandler returns new ReleaseHandler. -func NewReleaseHandler(log *zap.Logger, helmCfgFlags *genericclioptions.ConfigFlags) *ReleaseHandler { +func NewReleaseHandler(log *zap.Logger, helmCfgFlags *genericclioptions.ConfigFlags) (*ReleaseHandler, error) { return &ReleaseHandler{ - log: log, + log: log, + helmCfgFlags: helmCfgFlags, + actionConfigurationProducer: ActionConfigurationProducer, + }, nil +} + +// OnCreate checks whether a given Helm release is accessible this storage backend. +func (h *ReleaseHandler) OnCreate(_ context.Context, req *pb.OnCreateRequest) (*pb.OnCreateResponse, error) { + if _, _, err := h.fetchHelmRelease(req.TypeInstanceId, req.Context); err != nil { // check if accessible + return nil, err } + + return &pb.OnCreateResponse{}, nil } // GetValue returns a value for a given TypeInstance. -func (h *ReleaseHandler) GetValue(_ context.Context, _ *pb.GetValueRequest) (*pb.GetValueResponse, error) { - h.log.Info("Getting value") +func (h *ReleaseHandler) GetValue(_ context.Context, req *pb.GetValueRequest) (*pb.GetValueResponse, error) { + rel, relCtx, err := h.fetchHelmRelease(req.TypeInstanceId, req.Context) + if err != nil { + return nil, err + } + + releaseData := ReleaseDetails{ + Name: rel.Name, + Namespace: rel.Namespace, + Chart: ChartDetails{ + Name: rel.Chart.Metadata.Name, + Version: rel.Chart.Metadata.Version, + Repo: relCtx.ChartLocation, + }, + } + + value, err := json.Marshal(releaseData) + if err != nil { + return nil, errors.Wrap(err, "while marshaling response value") + } + return &pb.GetValueResponse{ - Value: []byte(`{"handler": "release"}`), + Value: value, }, nil } + +// OnUpdate checks whether a given Helm release is accessible this storage backend. +func (h *ReleaseHandler) OnUpdate(_ context.Context, req *pb.OnUpdateRequest) (*pb.OnUpdateResponse, error) { + if _, _, err := h.fetchHelmRelease(req.TypeInstanceId, req.Context); err != nil { // check if accessible + return nil, err + } + return &pb.OnUpdateResponse{}, nil +} + +// OnDelete is NOP. Currently, we are not sure whether the release should be deleted or this should be more an information +// that someone wants to deregister this TypeInstance. +func (h *ReleaseHandler) OnDelete(_ context.Context, _ *pb.OnDeleteRequest) (*pb.OnDeleteResponse, error) { + return &pb.OnDeleteResponse{}, nil +} + +// GetLockedBy is NOP. +func (h *ReleaseHandler) GetLockedBy(_ context.Context, _ *pb.GetLockedByRequest) (*pb.GetLockedByResponse, error) { + return &pb.GetLockedByResponse{}, nil +} + +// OnLock is NOP. +func (h *ReleaseHandler) OnLock(_ context.Context, _ *pb.OnLockRequest) (*pb.OnLockResponse, error) { + return &pb.OnLockResponse{}, nil +} + +// OnUnlock is NOP. +func (h *ReleaseHandler) OnUnlock(_ context.Context, _ *pb.OnUnlockRequest) (*pb.OnUnlockResponse, error) { + return &pb.OnUnlockResponse{}, nil +} + +func (h *ReleaseHandler) getReleaseContext(contextBytes []byte) (*ReleaseContext, error) { + var ctx ReleaseContext + err := json.Unmarshal(contextBytes, &ctx) + if err != nil { + return nil, h.internalError(errors.Wrap(err, "while unmarshaling context")) + } + + if ctx.Driver == nil { + ctx.Driver = ptr.String(defaultHelmDriver) + } + + return &ctx, nil +} + +func (h *ReleaseHandler) fetchHelmRelease(ti string, ctx []byte) (*release.Release, *ReleaseContext, error) { + relCtx, err := h.getReleaseContext(ctx) + if err != nil { + return nil, nil, err + } + + helmGet, err := h.newHelmGet(h.helmCfgFlags, *relCtx.Driver, relCtx.Namespace) + if err != nil { + return nil, nil, h.internalError(errors.Wrap(err, "while creating Helm get release client")) + } + + // NOTE: req.resourceVersion is ignored on purpose. + // Based on our contract we always return the latest Helm release revision. + helmGet.Version = latestRevisionIndicator + + rel, err := helmGet.Run(relCtx.Name) + switch { + case err == nil: + case errors.Is(err, driver.ErrReleaseNotFound): + return nil, nil, status.Error(codes.NotFound, fmt.Sprintf("Helm release '%s/%s' for TypeInstance '%s' was not found", relCtx.Namespace, relCtx.Name, ti)) + default: + return nil, nil, h.internalError(errors.Wrap(err, "while fetching Helm release")) + } + + return rel, relCtx, nil +} + +func (h *ReleaseHandler) newHelmGet(flags *genericclioptions.ConfigFlags, driver, ns string) (*action.Get, error) { + actionConfig, err := h.actionConfigurationProducer(flags, driver, ns) + if err != nil { + return nil, err + } + + return action.NewGet(actionConfig), nil +} + +func (h *ReleaseHandler) internalError(err error) error { + return status.Error(codes.Internal, err.Error()) +} diff --git a/internal/helm-storage-backend/release_test.go b/internal/helm-storage-backend/release_test.go new file mode 100644 index 000000000..4fc0e7847 --- /dev/null +++ b/internal/helm-storage-backend/release_test.go @@ -0,0 +1,520 @@ +package helmstoragebackend + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + kubefake "helm.sh/helm/v3/pkg/kube/fake" + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/storage" + "helm.sh/helm/v3/pkg/storage/driver" + "helm.sh/helm/v3/pkg/time" + "k8s.io/cli-runtime/pkg/genericclioptions" + + "capact.io/capact/internal/logger" + "capact.io/capact/internal/ptr" + pb "capact.io/capact/pkg/hub/api/grpc/storage_backend" +) + +func TestRelease_GetValue_Success(t *testing.T) { + tests := []struct { + name string + + givenDriver *string + givenTypeInstanceID string + givenResourceVersion uint32 + expectedDriver string + }{ + { + name: "should use default driver and return the latest release", + givenTypeInstanceID: "123", + givenDriver: nil, + expectedDriver: "secrets", + }, + { + name: "should use configmap driver and return the latest release", + givenTypeInstanceID: "123", + givenDriver: ptr.String("configmaps"), + expectedDriver: "configmaps", + }, + { + name: "should ignore resourceVersion and return the latest release", + givenTypeInstanceID: "123", + givenResourceVersion: 42, // should be ignored + expectedDriver: "secrets", + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + // given + const ( + releaseName = "test-get-release" + releaseNamespace = "test-get-namespace" + chartLocation = "http://example.com/charts" + ) + expHelmRelease := fixHelmRelease(releaseName, releaseNamespace) + expFlags := &genericclioptions.ConfigFlags{ClusterName: ptr.String("testing")} + mockConfigurationProducer := mockConfigurationProducer(t, expHelmRelease, expFlags, test.expectedDriver) + + givenReq := &pb.GetValueRequest{ + TypeInstanceId: test.givenTypeInstanceID, + ResourceVersion: test.givenResourceVersion, + Context: mustMarshal(t, ReleaseContext{ + Name: releaseName, + Namespace: releaseNamespace, + ChartLocation: chartLocation, + Driver: test.givenDriver, + }), + } + + expResponse := &pb.GetValueResponse{ + Value: mustMarshal(t, ReleaseDetails{ + Name: expHelmRelease.Name, + Namespace: expHelmRelease.Namespace, + Chart: ChartDetails{ + Name: expHelmRelease.Chart.Metadata.Name, + Version: expHelmRelease.Chart.Metadata.Version, + Repo: chartLocation, + }, + }), + } + + svc, err := NewReleaseHandler(logger.Noop(), expFlags) + svc.actionConfigurationProducer = mockConfigurationProducer + require.NoError(t, err) + + // when + outVal, gotErr := svc.GetValue(context.Background(), givenReq) + + // then + assert.NoError(t, gotErr) + assert.Equal(t, outVal, expResponse) + }) + } +} + +func TestRelease_GetValue_Failures(t *testing.T) { + // globally given + const ( + releaseName = "test-release" + releaseNamespace = "test-namespace" + ) + tests := []struct { + name string + + request *pb.GetValueRequest + internalError error + + expErrMsg string + }{ + { + name: "should return not found error if release name is wrong", + request: &pb.GetValueRequest{ + TypeInstanceId: "123", + Context: mustMarshal(t, ReleaseContext{ + Name: "other-release", + Namespace: releaseNamespace, + ChartLocation: "http://example.com/charts", + }), + }, + expErrMsg: "rpc error: code = NotFound desc = Helm release 'test-namespace/other-release' for TypeInstance '123' was not found", + }, + { + name: "should return not found error if release namespace is wrong", + request: &pb.GetValueRequest{ + TypeInstanceId: "123", + Context: mustMarshal(t, ReleaseContext{ + Name: releaseName, + Namespace: "other-ns", + ChartLocation: "http://example.com/charts", + }), + }, + expErrMsg: "rpc error: code = NotFound desc = Helm release 'other-ns/test-release' for TypeInstance '123' was not found", + }, + { + name: "should return internal error", + request: &pb.GetValueRequest{ + TypeInstanceId: "123", + Context: mustMarshal(t, ReleaseContext{ + Name: releaseName, + Namespace: "other-ns", + ChartLocation: "http://example.com/charts", + }), + }, + internalError: errors.New("internal error"), + expErrMsg: "rpc error: code = Internal desc = while creating Helm get release client: internal error", + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + // given + expHelmRelease := fixHelmRelease(releaseName, releaseNamespace) + expFlags := &genericclioptions.ConfigFlags{ClusterName: ptr.String("testing")} + + mockConfigurationProducer := func(inputFlags *genericclioptions.ConfigFlags, inputDriver, inputNs string) (*action.Configuration, error) { + if test.internalError != nil { + return nil, test.internalError + } + producer := mockConfigurationProducer(t, expHelmRelease, expFlags, "secrets") + return producer(inputFlags, inputDriver, inputNs) + } + + svc, err := NewReleaseHandler(logger.Noop(), expFlags) + svc.actionConfigurationProducer = mockConfigurationProducer + require.NoError(t, err) + + // when + outVal, gotErr := svc.GetValue(context.Background(), test.request) + + // then + assert.EqualError(t, gotErr, test.expErrMsg) + assert.Nil(t, outVal) + }) + } +} + +func TestRelease_OnCreate_Success(t *testing.T) { + // given + const ( + releaseName = "test-create-release" + releaseNamespace = "test-create-namespace" + releaseDriver = "configmap" + chartLocation = "http://example.com/charts" + ) + expHelmRelease := fixHelmRelease(releaseName, releaseNamespace) + expFlags := &genericclioptions.ConfigFlags{ClusterName: ptr.String("testing")} + mockConfigurationProducer := mockConfigurationProducer(t, expHelmRelease, expFlags, releaseDriver) + + givenReq := &pb.OnCreateRequest{ + TypeInstanceId: "42", + Context: mustMarshal(t, ReleaseContext{ + Name: releaseName, + Namespace: releaseNamespace, + ChartLocation: chartLocation, + Driver: ptr.String(releaseDriver), + }), + } + + svc, err := NewReleaseHandler(logger.Noop(), expFlags) + svc.actionConfigurationProducer = mockConfigurationProducer + require.NoError(t, err) + + // when + gotOut, gotErr := svc.OnCreate(context.Background(), givenReq) + + // then + assert.NoError(t, gotErr) + assert.Empty(t, gotOut) +} + +func TestRelease_OnCreate_Failures(t *testing.T) { + // globally given + const ( + releaseName = "test-release" + releaseNamespace = "test-namespace" + ) + tests := []struct { + name string + + request *pb.OnCreateRequest + internalError error + + expErrMsg string + }{ + { + name: "should return not found error if release name is wrong", + request: &pb.OnCreateRequest{ + TypeInstanceId: "123", + Context: mustMarshal(t, ReleaseContext{ + Name: "other-release", + Namespace: releaseNamespace, + ChartLocation: "http://example.com/charts", + }), + }, + expErrMsg: "rpc error: code = NotFound desc = Helm release 'test-namespace/other-release' for TypeInstance '123' was not found", + }, + { + name: "should return not found error if release namespace is wrong", + request: &pb.OnCreateRequest{ + TypeInstanceId: "123", + Context: mustMarshal(t, ReleaseContext{ + Name: releaseName, + Namespace: "other-ns", + ChartLocation: "http://example.com/charts", + }), + }, + expErrMsg: "rpc error: code = NotFound desc = Helm release 'other-ns/test-release' for TypeInstance '123' was not found", + }, + { + name: "should return internal error", + request: &pb.OnCreateRequest{ + TypeInstanceId: "123", + Context: mustMarshal(t, ReleaseContext{ + Name: releaseName, + Namespace: releaseNamespace, + ChartLocation: "http://example.com/charts", + }), + }, + internalError: errors.New("internal error"), + expErrMsg: "rpc error: code = Internal desc = while creating Helm get release client: internal error", + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + // given + expHelmRelease := fixHelmRelease(releaseName, releaseNamespace) + expFlags := &genericclioptions.ConfigFlags{ClusterName: ptr.String("testing")} + + mockConfigurationProducer := func(inputFlags *genericclioptions.ConfigFlags, inputDriver, inputNs string) (*action.Configuration, error) { + if test.internalError != nil { + return nil, test.internalError + } + producer := mockConfigurationProducer(t, expHelmRelease, expFlags, "secrets") + return producer(inputFlags, inputDriver, inputNs) + } + + svc, err := NewReleaseHandler(logger.Noop(), expFlags) + svc.actionConfigurationProducer = mockConfigurationProducer + require.NoError(t, err) + + // when + outVal, gotErr := svc.OnCreate(context.Background(), test.request) + + // then + assert.EqualError(t, gotErr, test.expErrMsg) + assert.Nil(t, outVal) + }) + } +} + +func TestRelease_OnUpdate_Success(t *testing.T) { + // given + const ( + releaseName = "test-update-release" + releaseNamespace = "test-update-namespace" + releaseDriver = "configmap" + chartLocation = "http://example.com/charts" + ) + expHelmRelease := fixHelmRelease(releaseName, releaseNamespace) + expFlags := &genericclioptions.ConfigFlags{ClusterName: ptr.String("testing")} + mockConfigurationProducer := mockConfigurationProducer(t, expHelmRelease, expFlags, releaseDriver) + + givenReq := &pb.OnUpdateRequest{ + TypeInstanceId: "42", + Context: mustMarshal(t, ReleaseContext{ + Name: releaseName, + Namespace: releaseNamespace, + ChartLocation: chartLocation, + Driver: ptr.String(releaseDriver), + }), + } + + svc, err := NewReleaseHandler(logger.Noop(), expFlags) + svc.actionConfigurationProducer = mockConfigurationProducer + require.NoError(t, err) + + // when + gotOut, gotErr := svc.OnUpdate(context.Background(), givenReq) + + // then + assert.NoError(t, gotErr) + assert.Empty(t, gotOut) +} + +func TestRelease_OnUpdate_Failures(t *testing.T) { + // globally given + const ( + releaseName = "test-release" + releaseNamespace = "test-namespace" + ) + tests := []struct { + name string + + request *pb.OnUpdateRequest + internalError error + + expErrMsg string + }{ + { + name: "should return not found error if release name is wrong", + request: &pb.OnUpdateRequest{ + TypeInstanceId: "123", + Context: mustMarshal(t, ReleaseContext{ + Name: "other-release", + Namespace: releaseNamespace, + ChartLocation: "http://example.com/charts", + }), + }, + expErrMsg: "rpc error: code = NotFound desc = Helm release 'test-namespace/other-release' for TypeInstance '123' was not found", + }, + { + name: "should return not found error if release namespace is wrong", + request: &pb.OnUpdateRequest{ + TypeInstanceId: "123", + Context: mustMarshal(t, ReleaseContext{ + Name: releaseName, + Namespace: "other-ns", + ChartLocation: "http://example.com/charts", + }), + }, + expErrMsg: "rpc error: code = NotFound desc = Helm release 'other-ns/test-release' for TypeInstance '123' was not found", + }, + { + name: "should return internal error", + request: &pb.OnUpdateRequest{ + TypeInstanceId: "123", + Context: mustMarshal(t, ReleaseContext{ + Name: releaseName, + Namespace: releaseNamespace, + ChartLocation: "http://example.com/charts", + }), + }, + internalError: errors.New("internal error"), + expErrMsg: "rpc error: code = Internal desc = while creating Helm get release client: internal error", + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + // given + expHelmRelease := fixHelmRelease(releaseName, releaseNamespace) + expFlags := &genericclioptions.ConfigFlags{ClusterName: ptr.String("testing")} + + mockConfigurationProducer := func(inputFlags *genericclioptions.ConfigFlags, inputDriver, inputNs string) (*action.Configuration, error) { + if test.internalError != nil { + return nil, test.internalError + } + producer := mockConfigurationProducer(t, expHelmRelease, expFlags, "secrets") + return producer(inputFlags, inputDriver, inputNs) + } + + svc, err := NewReleaseHandler(logger.Noop(), expFlags) + svc.actionConfigurationProducer = mockConfigurationProducer + require.NoError(t, err) + + // when + outVal, gotErr := svc.OnUpdate(context.Background(), test.request) + + // then + assert.EqualError(t, gotErr, test.expErrMsg) + assert.Nil(t, outVal) + }) + } +} + +func TestRelease_NOP_Methods(t *testing.T) { + // globally given + tests := []struct { + name string + handler func(ctx context.Context, svc *ReleaseHandler) (interface{}, error) + }{ + { + name: "no operation for OnDelete", + handler: func(ctx context.Context, svc *ReleaseHandler) (interface{}, error) { + return svc.OnDelete(ctx, nil) + }, + }, + { + name: "no operation for GetLockedBy", + handler: func(ctx context.Context, svc *ReleaseHandler) (interface{}, error) { + return svc.GetLockedBy(ctx, nil) + }, + }, + { + name: "no operation for OnLock", + handler: func(ctx context.Context, svc *ReleaseHandler) (interface{}, error) { + return svc.OnLock(ctx, nil) + }, + }, + { + name: "no operation for OnUnlock", + handler: func(ctx context.Context, svc *ReleaseHandler) (interface{}, error) { + return svc.OnUnlock(ctx, nil) + }, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + // given + producerCalled := false + mockConfigurationProducer := func(_ *genericclioptions.ConfigFlags, _, _ string) (*action.Configuration, error) { + producerCalled = true + return nil, nil + } + svc, err := NewReleaseHandler(logger.Noop(), nil) + svc.actionConfigurationProducer = mockConfigurationProducer + require.NoError(t, err) + + // when + outVal, gotErr := test.handler(context.Background(), svc) + + // then + assert.NoError(t, gotErr) + assert.False(t, producerCalled) + assert.Empty(t, outVal) + }) + } +} + +func mockConfigurationProducer(t *testing.T, expHelmRelease *release.Release, expFlags *genericclioptions.ConfigFlags, expDriver string) actionConfigurationProducerFn { + t.Helper() + inMemoryDriver := driver.NewMemory() + err := inMemoryDriver.Create("1", expHelmRelease) + require.NoError(t, err) + + return func(inputFlags *genericclioptions.ConfigFlags, inputDriver, inputNs string) (*action.Configuration, error) { + assert.Equal(t, expFlags, inputFlags) + assert.Equal(t, expDriver, inputDriver) + + inMemoryDriver.SetNamespace(inputNs) + return &action.Configuration{ + Releases: storage.Init(inMemoryDriver), + KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: ioutil.Discard}}, + }, nil + } +} + +func mustMarshal(t *testing.T, v interface{}) []byte { + t.Helper() + out, err := json.Marshal(v) + if err != nil { + t.Fatal(err) + } + return out +} + +func fixHelmRelease(name, ns string) *release.Release { + now := time.Now() + return &release.Release{ + Name: name, + Namespace: ns, + Info: &release.Info{ + FirstDeployed: now, + LastDeployed: now, + Description: "Named Release Stub", + }, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{ + Name: fmt.Sprintf("%s-chart", name), + Version: "0.1.0", + }, + }, + } +} diff --git a/internal/helm-storage-backend/showcase_test.go b/internal/helm-storage-backend/showcase_test.go new file mode 100644 index 000000000..5dc070ba2 --- /dev/null +++ b/internal/helm-storage-backend/showcase_test.go @@ -0,0 +1,96 @@ +package helmstoragebackend + +import ( + "context" + "encoding/json" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + + pb "capact.io/capact/pkg/hub/api/grpc/storage_backend" +) + +// To run this test, execute: +// GRPC_SECRET_STORAGE_BACKEND_ADDR=":50051" go test ./internal/helm-storage-backend/... -run "^TestShowcase$" -v -count 1 +func TestShowcase(t *testing.T) { + srvAddr := os.Getenv("GRPC_SECRET_STORAGE_BACKEND_ADDR") + if srvAddr == "" { + t.Skip() + } + + conn, err := grpc.Dial(srvAddr, grpc.WithInsecure()) + require.NoError(t, err) + + ctx := context.Background() + client := pb.NewStorageBackendClient(conn) + + // ===== GET ===== + out, err := client.GetValue(ctx, &pb.GetValueRequest{ + TypeInstanceId: "42", + Context: mustMarshal(t, ReleaseContext{ + Name: "example-release", + Namespace: "default", + ChartLocation: "https://charts.bitnami.com/bitnami", + })}) + + require.NoError(t, err) + + details := &ReleaseDetails{} + require.NoError(t, json.Unmarshal(out.Value, details)) + + fmt.Printf("GetValue for valid release") + fmt.Printf("\t\t Name: %s\n", details.Name) + fmt.Printf("\t\t Namespace: %s\n", details.Namespace) + fmt.Printf("\t\t Chart.Name: %s\n", details.Chart.Name) + fmt.Printf("\t\t Chart.Version: %s\n", details.Chart.Version) + fmt.Printf("\t\t Chart.Repo: %s\n", details.Chart.Repo) + + _, err = client.GetValue(ctx, &pb.GetValueRequest{ + TypeInstanceId: "42", + Context: mustMarshal(t, ReleaseContext{ + Name: "fake-release", + Namespace: "default", + ChartLocation: "https://charts.bitnami.com/bitnami", + })}) + + fmt.Printf("GetValue err if release doesn't exist: %v\n", err) + + // ===== UPDATE ===== + _, err = client.OnUpdate(ctx, &pb.OnUpdateRequest{ + Context: mustMarshal(t, ReleaseContext{ + Name: "example-release", + Namespace: "default", + ChartLocation: "https://charts.bitnami.com/bitnami", + })}) + fmt.Printf("OnUpdate err if release exists: %v\n", err) + + _, err = client.OnUpdate(ctx, &pb.OnUpdateRequest{ + TypeInstanceId: "42", + Context: mustMarshal(t, ReleaseContext{ + Name: "fake-release", + Namespace: "default", + ChartLocation: "https://charts.bitnami.com/bitnami", + })}) + fmt.Printf("OnUpdate err if release doesn't exist: %v\n", err) + + // ===== CREATE ===== + _, err = client.OnCreate(ctx, &pb.OnCreateRequest{ + Context: mustMarshal(t, ReleaseContext{ + Name: "example-release", + Namespace: "default", + ChartLocation: "https://charts.bitnami.com/bitnami", + })}) + fmt.Printf("OnCreate err if release exists: %v\n", err) + + _, err = client.OnCreate(ctx, &pb.OnCreateRequest{ + TypeInstanceId: "42", + Context: mustMarshal(t, ReleaseContext{ + Name: "fake-release", + Namespace: "default", + ChartLocation: "https://charts.bitnami.com/bitnami", + })}) + fmt.Printf("OnCreate err if release doesn't exist: %v\n", err) +} diff --git a/pkg/runner/cloudsql/create.go b/pkg/runner/cloudsql/create.go index 9812d68b5..cd86ea1c1 100644 --- a/pkg/runner/cloudsql/create.go +++ b/pkg/runner/cloudsql/create.go @@ -6,6 +6,7 @@ import ( "html/template" "io/ioutil" "os" + "path/filepath" "time" "capact.io/capact/pkg/runner" @@ -174,7 +175,7 @@ func (a *createAction) createAdditionalOutputFile(path string, args *OutputArgs, return errors.Wrap(err, "failed to load template") } - fd, err := os.Create(path) + fd, err := os.Create(filepath.Clean(path)) if err != nil { return errors.Wrap(err, "cannot open output file to write") }