feat: [CDE-430]: Providing a user friendly error message whenever an instance goes to error state. (#2977)

* feat: [CDE-430]: Providing a user friendly error message whenever an instance goes to error state.
pull/3586/head
Dhruv Dhruv 2024-11-12 05:58:28 +00:00 committed by Harness
parent 42c76929c5
commit 1781c3be70
13 changed files with 159 additions and 61 deletions

View File

@ -226,7 +226,11 @@ func PullImage(
pullResponse, err := dockerClient.ImagePull(ctx, imageName, image.PullOptions{})
defer func() {
if closingErr := pullResponse.Close(); closingErr != nil {
if pullResponse == nil {
return
}
closingErr := pullResponse.Close()
if closingErr != nil {
log.Warn().Err(closingErr).Msg("failed to close image pull response")
}
}()

View File

@ -23,25 +23,25 @@ import (
type Orchestrator interface {
// TriggerStartGitspace fetches the infra resources configured for the gitspace and triggers the infra provisioning.
TriggerStartGitspace(ctx context.Context, gitspaceConfig types.GitspaceConfig) error
TriggerStartGitspace(ctx context.Context, gitspaceConfig types.GitspaceConfig) *types.GitspaceError
// ResumeStartGitspace saves the provisioned infra, resolves the code repo details & creates the Gitspace container.
ResumeStartGitspace(
ctx context.Context,
gitspaceConfig types.GitspaceConfig,
provisionedInfra types.Infrastructure,
) (types.GitspaceInstance, error)
) (types.GitspaceInstance, *types.GitspaceError)
// TriggerStopGitspace stops the Gitspace container and triggers infra deprovisioning to deprovision
// all the infra resources which are not required to restart the Gitspace.
TriggerStopGitspace(ctx context.Context, gitspaceConfig types.GitspaceConfig) error
TriggerStopGitspace(ctx context.Context, gitspaceConfig types.GitspaceConfig) *types.GitspaceError
// ResumeStopGitspace saves the deprovisioned infra details.
ResumeStopGitspace(
ctx context.Context,
gitspaceConfig types.GitspaceConfig,
stoppedInfra types.Infrastructure,
) (enum.GitspaceInstanceStateType, error)
) (enum.GitspaceInstanceStateType, *types.GitspaceError)
// TriggerCleanupInstanceResources cleans up all the resources exclusive to gitspace instance.
TriggerCleanupInstanceResources(ctx context.Context, gitspaceConfig types.GitspaceConfig) error

View File

@ -30,6 +30,7 @@ import (
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
"github.com/gotidy/ptr"
"github.com/rs/zerolog/log"
)
@ -37,19 +38,25 @@ func (o orchestrator) ResumeStartGitspace(
ctx context.Context,
gitspaceConfig types.GitspaceConfig,
provisionedInfra types.Infrastructure,
) (types.GitspaceInstance, error) {
) (types.GitspaceInstance, *types.GitspaceError) {
gitspaceInstance := gitspaceConfig.GitspaceInstance
gitspaceInstance.State = enum.GitspaceInstanceStateError
secretResolver, err := o.getSecretResolver(gitspaceInstance.AccessType)
if err != nil {
log.Err(err).Msgf("could not find secret resolver for type: %s", gitspaceInstance.AccessType)
return *gitspaceInstance, err
return *gitspaceInstance, &types.GitspaceError{
Error: err,
ErrorMessage: ptr.String(err.Error()),
}
}
rootSpaceID, _, err := paths.DisectRoot(gitspaceConfig.SpacePath)
if err != nil {
log.Err(err).Msgf("unable to find root space id from space path: %s", gitspaceConfig.SpacePath)
return *gitspaceInstance, err
return *gitspaceInstance, &types.GitspaceError{
Error: err,
ErrorMessage: ptr.String(err.Error()),
}
}
resolvedSecret, err := secretResolver.Resolve(ctx, secret.ResolutionContext{
UserIdentifier: gitspaceConfig.GitspaceUser.Identifier,
@ -60,13 +67,19 @@ func (o orchestrator) ResumeStartGitspace(
if err != nil {
log.Err(err).Msgf("could not resolve secret type: %s, ref: %s",
gitspaceInstance.AccessType, *gitspaceInstance.AccessKeyRef)
return *gitspaceInstance, err
return *gitspaceInstance, &types.GitspaceError{
Error: err,
ErrorMessage: ptr.String(err.Error()),
}
}
gitspaceInstance.AccessKey = &resolvedSecret.SecretValue
ideSvc, err := o.getIDEService(gitspaceConfig)
if err != nil {
return *gitspaceInstance, err
return *gitspaceInstance, &types.GitspaceError{
Error: err,
ErrorMessage: ptr.String(err.Error()),
}
}
idePort := ideSvc.Port()
@ -75,35 +88,47 @@ func (o orchestrator) ResumeStartGitspace(
if err != nil {
o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraProvisioningFailed)
return *gitspaceInstance, fmt.Errorf(
"cannot provision infrastructure for ID %s: %w", gitspaceConfig.InfraProviderResource.UID, err)
return *gitspaceInstance, &types.GitspaceError{
Error: fmt.Errorf("cannot provision infrastructure for ID %s: %w",
gitspaceConfig.InfraProviderResource.UID, err),
ErrorMessage: ptr.String(err.Error()),
}
}
if provisionedInfra.Status != enum.InfraStatusProvisioned {
o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraProvisioningFailed)
return *gitspaceInstance, fmt.Errorf(
infraStateErr := fmt.Errorf(
"infra state is %v, should be %v for gitspace instance identifier %s",
provisionedInfra.Status,
enum.InfraStatusProvisioned,
gitspaceConfig.GitspaceInstance.Identifier,
)
return *gitspaceInstance, &types.GitspaceError{
Error: infraStateErr,
ErrorMessage: ptr.String(infraStateErr.Error()),
}
}
o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraProvisioningCompleted)
scmResolvedDetails, err := o.scm.GetSCMRepoDetails(ctx, gitspaceConfig)
if err != nil {
return *gitspaceInstance, fmt.Errorf(
"failed to fetch code repo details for gitspace config ID %w %d", err, gitspaceConfig.ID)
return *gitspaceInstance, &types.GitspaceError{
Error: fmt.Errorf("failed to fetch code repo details for gitspace config ID %d: %w",
gitspaceConfig.ID, err),
ErrorMessage: ptr.String(err.Error()),
}
}
o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentConnectStart)
err = o.containerOrchestrator.Status(ctx, provisionedInfra)
if err != nil {
o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentConnectFailed)
return *gitspaceInstance, fmt.Errorf("couldn't call the agent health API: %w", err)
agentUnreachableErr := fmt.Errorf("couldn't call the agent health API: %w", err)
return *gitspaceInstance, &types.GitspaceError{
Error: agentUnreachableErr,
ErrorMessage: ptr.String(agentUnreachableErr.Error()),
}
}
o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentConnectCompleted)
@ -118,7 +143,10 @@ func (o orchestrator) ResumeStartGitspace(
if err != nil {
o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentGitspaceCreationFailed)
return *gitspaceInstance, fmt.Errorf("couldn't call the agent start API: %w", err)
return *gitspaceInstance, &types.GitspaceError{
Error: fmt.Errorf("couldn't call the agent start API: %w", err),
ErrorMessage: ptr.String(err.Error()), // TODO: Fetch explicit error msg from container orchestrator
}
}
o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentGitspaceCreationCompleted)
@ -200,26 +228,32 @@ func (o orchestrator) ResumeStopGitspace(
ctx context.Context,
gitspaceConfig types.GitspaceConfig,
stoppedInfra types.Infrastructure,
) (enum.GitspaceInstanceStateType, error) {
) (enum.GitspaceInstanceStateType, *types.GitspaceError) {
instanceState := enum.GitspaceInstanceStateError
err := o.infraProvisioner.ResumeStop(ctx, gitspaceConfig, stoppedInfra)
if err != nil {
o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraStopFailed)
return instanceState, fmt.Errorf(
"cannot stop provisioned infrastructure with ID %s: %w", gitspaceConfig.InfraProviderResource.UID, err)
infraStopErr := fmt.Errorf("cannot stop provisioned infrastructure with ID %s: %w",
gitspaceConfig.InfraProviderResource.UID, err)
return instanceState, &types.GitspaceError{
Error: infraStopErr,
ErrorMessage: ptr.String(infraStopErr.Error()), // TODO: Fetch explicit error msg
}
}
if stoppedInfra.Status != enum.InfraStatusDestroyed &&
stoppedInfra.Status != enum.InfraStatusStopped {
o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraStopFailed)
return instanceState, fmt.Errorf(
incorrectInfraStateErr := fmt.Errorf(
"infra state is %v, should be %v for gitspace instance identifier %s",
stoppedInfra.Status,
enum.InfraStatusDestroyed,
gitspaceConfig.GitspaceInstance.Identifier)
return instanceState, &types.GitspaceError{
Error: incorrectInfraStateErr,
ErrorMessage: ptr.String(incorrectInfraStateErr.Error()), // TODO: Fetch explicit error msg
}
}
o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraStopCompleted)

View File

@ -29,6 +29,7 @@ import (
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
"github.com/gotidy/ptr"
"github.com/rs/zerolog/log"
"golang.org/x/exp/slices"
)
@ -80,13 +81,16 @@ func NewOrchestrator(
func (o orchestrator) TriggerStartGitspace(
ctx context.Context,
gitspaceConfig types.GitspaceConfig,
) error {
) *types.GitspaceError {
o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeFetchDevcontainerStart)
scmResolvedDetails, err := o.scm.GetSCMRepoDetails(ctx, gitspaceConfig)
if err != nil {
o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeFetchDevcontainerFailed)
return fmt.Errorf(
"failed to fetch code repo details for gitspace config ID %w %d", err, gitspaceConfig.ID)
return &types.GitspaceError{
Error: fmt.Errorf("failed to fetch code repo details for gitspace config ID %d: %w",
gitspaceConfig.ID, err),
ErrorMessage: ptr.String(err.Error()),
}
}
devcontainerConfig := scmResolvedDetails.DevcontainerConfig
@ -94,7 +98,11 @@ func (o orchestrator) TriggerStartGitspace(
requiredGitspacePorts, err := o.getPortsRequiredForGitspace(gitspaceConfig, devcontainerConfig)
if err != nil {
return fmt.Errorf("cannot get the ports required for gitspace during start: %w", err)
err = fmt.Errorf("cannot get the ports required for gitspace during start: %w", err)
return &types.GitspaceError{
Error: err,
ErrorMessage: ptr.String(err.Error()),
}
}
o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraProvisioningStart)
@ -102,9 +110,11 @@ func (o orchestrator) TriggerStartGitspace(
err = o.infraProvisioner.TriggerProvision(ctx, gitspaceConfig, requiredGitspacePorts)
if err != nil {
o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraProvisioningFailed)
return fmt.Errorf(
"cannot trigger provision infrastructure for ID %s: %w", gitspaceConfig.InfraProviderResource.UID, err)
return &types.GitspaceError{
Error: fmt.Errorf("cannot trigger provision infrastructure for ID %s: %w",
gitspaceConfig.InfraProviderResource.UID, err),
ErrorMessage: ptr.String(err.Error()), // TODO: Fetch explicit error msg from infra provisioner.
}
}
return nil
@ -113,20 +123,26 @@ func (o orchestrator) TriggerStartGitspace(
func (o orchestrator) TriggerStopGitspace(
ctx context.Context,
gitspaceConfig types.GitspaceConfig,
) error {
) *types.GitspaceError {
infra, err := o.getProvisionedInfra(ctx, gitspaceConfig,
[]enum.InfraStatus{enum.InfraStatusProvisioned, enum.InfraStatusStopped})
if err != nil {
return fmt.Errorf(
"unable to find provisioned infra while triggering stop for gitspace instance %s: %w",
gitspaceConfig.GitspaceInstance.Identifier, err)
infraNotFoundErr := fmt.Errorf("unable to find provisioned infra while triggering stop for gitspace "+
"instance %s: %w", gitspaceConfig.GitspaceInstance.Identifier, err)
return &types.GitspaceError{
Error: infraNotFoundErr,
ErrorMessage: ptr.String(infraNotFoundErr.Error()), // TODO: Fetch explicit error msg
}
}
if gitspaceConfig.GitspaceInstance.State == enum.GitspaceInstanceStateRunning ||
gitspaceConfig.GitspaceInstance.State == enum.GitspaceInstanceStateStopping {
err = o.stopGitspaceContainer(ctx, gitspaceConfig, *infra)
}
if err != nil {
return err
if err != nil {
return &types.GitspaceError{
Error: err,
ErrorMessage: ptr.String(err.Error()), // TODO: Fetch explicit error msg
}
}
}
o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraStopStart)
@ -134,9 +150,12 @@ func (o orchestrator) TriggerStopGitspace(
err = o.infraProvisioner.TriggerStop(ctx, gitspaceConfig.InfraProviderResource, *infra)
if err != nil {
o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraStopFailed)
return fmt.Errorf(
"cannot trigger stop infrastructure with ID %s: %w", gitspaceConfig.InfraProviderResource.UID, err)
infraStopErr := fmt.Errorf("cannot trigger stop infrastructure with ID %s: %w",
gitspaceConfig.InfraProviderResource.UID, err)
return &types.GitspaceError{
Error: infraStopErr,
ErrorMessage: ptr.String(infraStopErr.Error()), // TODO: Fetch explicit error msg
}
}
return nil

View File

@ -61,7 +61,7 @@ func (c *Service) submitAsyncOps(
config *types.GitspaceConfig,
action enum.GitspaceActionType,
) {
errChannel := make(chan error)
errChannel := make(chan *types.GitspaceError)
submitCtx := context.WithoutCancel(ctx)
gitspaceTimedOutInMins := time.Duration(c.config.Gitspace.ProvisionTimeoutInMins) * time.Minute
@ -69,20 +69,22 @@ func (c *Service) submitAsyncOps(
go c.asyncOperation(ttlExecuteContext, *config, action, errChannel)
var err error
var err *types.GitspaceError
go func() {
select {
case <-ttlExecuteContext.Done():
if ttlExecuteContext.Err() != nil {
err = ttlExecuteContext.Err()
err = &types.GitspaceError{
Error: ttlExecuteContext.Err(),
}
}
case err = <-errChannel:
}
if err != nil {
log.Err(err).Msgf("error during async execution for %s", config.GitspaceInstance.Identifier)
log.Err(err.Error).Msgf("error during async execution for %s", config.GitspaceInstance.Identifier)
config.GitspaceInstance.State = enum.GitspaceInstanceStateError
config.GitspaceInstance.ErrorMessage = err.ErrorMessage
updateErr := c.UpdateInstance(submitCtx, config.GitspaceInstance)
if updateErr != nil {
log.Err(updateErr).Msgf(
@ -105,11 +107,11 @@ func (c *Service) asyncOperation(
ctxWithTimedOut context.Context,
config types.GitspaceConfig,
action enum.GitspaceActionType,
errChannel chan error,
errChannel chan *types.GitspaceError,
) {
defer close(errChannel)
var orchestrateErr error
var orchestrateErr *types.GitspaceError
switch action {
case enum.GitspaceActionTypeStart:
@ -117,7 +119,7 @@ func (c *Service) asyncOperation(
err := c.UpdateInstance(ctxWithTimedOut, config.GitspaceInstance)
if err != nil {
log.Err(err).Msgf(
"failed to update gitspace instance during exec %q", config.GitspaceInstance.Identifier)
"failed to update gitspace instance during exec %s", config.GitspaceInstance.Identifier)
}
orchestrateErr = c.orchestrator.TriggerStartGitspace(ctxWithTimedOut, config)
case enum.GitspaceActionTypeStop:
@ -125,13 +127,15 @@ func (c *Service) asyncOperation(
err := c.UpdateInstance(ctxWithTimedOut, config.GitspaceInstance)
if err != nil {
log.Err(err).Msgf(
"failed to update gitspace instance during exec %q", config.GitspaceInstance.Identifier)
"failed to update gitspace instance during exec %s", config.GitspaceInstance.Identifier)
}
orchestrateErr = c.orchestrator.TriggerStopGitspace(ctxWithTimedOut, config)
}
if orchestrateErr != nil {
errChannel <- fmt.Errorf("failed to start/stop gitspace: %s %w", config.Identifier, orchestrateErr)
orchestrateErr.Error =
fmt.Errorf("failed to start/stop gitspace: %s %w", config.Identifier, orchestrateErr.Error)
errChannel <- orchestrateErr
}
}

View File

@ -69,8 +69,8 @@ func (s *Service) handleGitspaceInfraEvent(
updatedInstance, resumeStartErr := s.orchestrator.ResumeStartGitspace(ctx, *config, payload.Infra)
if resumeStartErr != nil {
s.emitGitspaceConfigEvent(ctx, config, enum.GitspaceEventTypeGitspaceActionStartFailed)
err = fmt.Errorf("failed to resume start gitspace: %w", resumeStartErr)
updatedInstance.ErrorMessage = resumeStartErr.ErrorMessage
err = fmt.Errorf("failed to resume start gitspace: %w", resumeStartErr.Error)
}
instance = &updatedInstance
@ -79,8 +79,8 @@ func (s *Service) handleGitspaceInfraEvent(
instanceState, resumeStopErr := s.orchestrator.ResumeStopGitspace(ctx, *config, payload.Infra)
if resumeStopErr != nil {
s.emitGitspaceConfigEvent(ctx, config, enum.GitspaceEventTypeGitspaceActionStopFailed)
err = fmt.Errorf("failed to resume stop gitspace: %w", resumeStopErr)
instance.ErrorMessage = resumeStopErr.ErrorMessage
err = fmt.Errorf("failed to resume stop gitspace: %w", resumeStopErr.Error)
}
instance.State = instanceState

View File

@ -54,7 +54,8 @@ const (
gits_last_heartbeat,
gits_active_time_started,
gits_active_time_ended,
gits_has_git_changes`
gits_has_git_changes,
gits_error_message`
gitspaceInstanceSelectColumns = "gits_id," + gitspaceInstanceInsertColumns
gitspaceInstanceTable = `gitspaces`
)
@ -80,6 +81,7 @@ type gitspaceInstance struct {
ActiveTimeStarted null.Int `db:"gits_active_time_started"`
ActiveTimeEnded null.Int `db:"gits_active_time_ended"`
HasGitChanges null.Bool `db:"gits_has_git_changes"`
ErrorMessage null.String `db:"gits_error_message"`
}
// NewGitspaceInstanceStore returns a new GitspaceInstanceStore.
@ -213,6 +215,7 @@ func (g gitspaceInstanceStore) Create(ctx context.Context, gitspaceInstance *typ
gitspaceInstance.ActiveTimeStarted,
gitspaceInstance.ActiveTimeEnded,
gitspaceInstance.HasGitChanges,
gitspaceInstance.ErrorMessage,
).
Suffix(ReturningClause + "gits_id")
sql, args, err := stmt.ToSql()
@ -242,6 +245,7 @@ func (g gitspaceInstanceStore) Update(
Set("gits_active_time_ended", gitspaceInstance.ActiveTimeEnded).
Set("gits_total_time_used", gitspaceInstance.TotalTimeUsed).
Set("gits_has_git_changes", gitspaceInstance.HasGitChanges).
Set("gits_error_message", gitspaceInstance.ErrorMessage).
Set("gits_updated", gitspaceInstance.Updated).
Where("gits_id = ?", gitspaceInstance.ID)
sql, args, err := stmt.ToSql()
@ -408,6 +412,7 @@ func mapDBToGitspaceInstance(
ActiveTimeEnded: in.ActiveTimeEnded.Ptr(),
ActiveTimeStarted: in.ActiveTimeStarted.Ptr(),
HasGitChanges: in.HasGitChanges.Ptr(),
ErrorMessage: in.ErrorMessage.Ptr(),
}
return res, nil
}
@ -443,18 +448,24 @@ func validateActiveTimeDetails(gitspaceInstance *types.GitspaceInstance) {
gitspaceInstance.TotalTimeUsed != 0) {
log.Warn().Msgf(
"instance is missing active time start or has incorrect end/total timestamps, details: "+
" identifier %s state %s active time start %d active time end %d total time used %d",
" identifier %s state %s active time start %d active time end %d total time used %d", // nolint:goconst
gitspaceInstance.Identifier, gitspaceInstance.State, gitspaceInstance.ActiveTimeStarted,
gitspaceInstance.ActiveTimeEnded, gitspaceInstance.TotalTimeUsed)
}
if (gitspaceInstance.State == enum.GitspaceInstanceStateDeleted ||
gitspaceInstance.State == enum.GitspaceInstanceStateStopping ||
gitspaceInstance.State == enum.GitspaceInstanceStateError) &&
gitspaceInstance.State == enum.GitspaceInstanceStateStopping) &&
(gitspaceInstance.ActiveTimeStarted == nil ||
gitspaceInstance.ActiveTimeEnded == nil ||
gitspaceInstance.TotalTimeUsed == 0) {
log.Warn().Msgf("instance is missing active time start/end/total timestamp, details: "+
" identifier %s state %s active time start %d active time end %d total time used %d",
" identifier %s state %s active time start %d active time end %d total time used %d", // nolint:goconst
gitspaceInstance.Identifier, gitspaceInstance.State, gitspaceInstance.ActiveTimeStarted,
gitspaceInstance.ActiveTimeEnded, gitspaceInstance.TotalTimeUsed)
}
if gitspaceInstance.State == enum.GitspaceInstanceStateError &&
(gitspaceInstance.ActiveTimeStarted == nil) != (gitspaceInstance.ActiveTimeEnded == nil) {
log.Warn().Msgf("instance has incorrect active time start/end/total timestamp, details: "+
" identifier %s state %s active time start %d active time end %d total time used %d", // nolint:goconst
gitspaceInstance.Identifier, gitspaceInstance.State, gitspaceInstance.ActiveTimeStarted,
gitspaceInstance.ActiveTimeEnded, gitspaceInstance.TotalTimeUsed)
}

View File

@ -0,0 +1 @@
ALTER TABLE gitspaces DROP COLUMN gits_error_message;

View File

@ -0,0 +1 @@
ALTER TABLE gitspaces ADD COLUMN gits_error_message TEXT;

View File

@ -0,0 +1 @@
ALTER TABLE gitspaces DROP COLUMN gits_error_message;

View File

@ -0,0 +1 @@
ALTER TABLE gitspaces ADD COLUMN gits_error_message TEXT;

View File

@ -78,6 +78,7 @@ type GitspaceInstance struct {
ActiveTimeStarted *int64 `json:"active_time_started,omitempty"`
ActiveTimeEnded *int64 `json:"active_time_ended,omitempty"`
HasGitChanges *bool `json:"has_git_changes,omitempty"`
ErrorMessage *string `json:"error_message,omitempty"`
}
type GitspaceFilter struct {

21
types/gitspace_error.go Normal file
View File

@ -0,0 +1,21 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package types
// GitspaceError wraps any error and provides a user-friendly error message which can be relayed back to UI.
type GitspaceError struct {
Error error
ErrorMessage *string
}