Merge remote-tracking branch 'origin' into abhinav/CODE-853

This commit is contained in:
Abhinav Singh 2023-09-13 17:31:45 -07:00
commit a309b44f0e
132 changed files with 4176 additions and 1042 deletions

3
.npmrc
View File

@ -1,3 +0,0 @@
@harness:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_ACCESS_TOKEN}
always-auth=true

View File

@ -14,7 +14,6 @@ ARG GITHUB_ACCESS_TOKEN
# RUN npm ci --omit=dev # RUN npm ci --omit=dev
COPY ./web . COPY ./web .
COPY .npmrc /root/.npmrc
RUN yarn && yarn build && yarn cache clean RUN yarn && yarn build && yarn cache clean
@ -70,18 +69,21 @@ RUN apk --update add ca-certificates
# ---------------------------------------------------------# # ---------------------------------------------------------#
FROM alpine/git:2.36.3 as final FROM alpine/git:2.36.3 as final
RUN adduser -u 1001 -D -h /app iamuser
# setup app dir and its content # setup app dir and its content
WORKDIR /app WORKDIR /app
RUN chown -R 1001:1001 /app VOLUME /data
COPY --from=builder --chown=1001:1001 --chmod=700 /app/gitness /app/gitness
ENV XDG_CACHE_HOME /data
ENV GITRPC_SERVER_GIT_ROOT /data
ENV GITNESS_DATABASE_DRIVER sqlite3
ENV GITNESS_DATABASE_DATASOURCE /data/database.sqlite
ENV GITNESS_METRIC_ENABLED=true
ENV GITNESS_METRIC_ENDPOINT=https://stats.drone.ci/api/v1/gitness
COPY --from=builder /app/gitness /app/gitness
COPY --from=cert-image /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=cert-image /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
EXPOSE 3000 EXPOSE 3000
EXPOSE 3001 EXPOSE 3001
USER 1001
ENTRYPOINT [ "/app/gitness", "server" ] ENTRYPOINT [ "/app/gitness", "server" ]

View File

@ -41,8 +41,8 @@ import (
"github.com/harness/gitness/internal/bootstrap" "github.com/harness/gitness/internal/bootstrap"
gitevents "github.com/harness/gitness/internal/events/git" gitevents "github.com/harness/gitness/internal/events/git"
pullreqevents "github.com/harness/gitness/internal/events/pullreq" pullreqevents "github.com/harness/gitness/internal/events/pullreq"
"github.com/harness/gitness/internal/pipeline/canceler"
"github.com/harness/gitness/internal/pipeline/commit" "github.com/harness/gitness/internal/pipeline/commit"
eventsstream "github.com/harness/gitness/internal/pipeline/events"
"github.com/harness/gitness/internal/pipeline/file" "github.com/harness/gitness/internal/pipeline/file"
"github.com/harness/gitness/internal/pipeline/manager" "github.com/harness/gitness/internal/pipeline/manager"
"github.com/harness/gitness/internal/pipeline/runner" "github.com/harness/gitness/internal/pipeline/runner"
@ -57,6 +57,7 @@ import (
pullreqservice "github.com/harness/gitness/internal/services/pullreq" pullreqservice "github.com/harness/gitness/internal/services/pullreq"
"github.com/harness/gitness/internal/services/trigger" "github.com/harness/gitness/internal/services/trigger"
"github.com/harness/gitness/internal/services/webhook" "github.com/harness/gitness/internal/services/webhook"
"github.com/harness/gitness/internal/sse"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/internal/store/cache" "github.com/harness/gitness/internal/store/cache"
"github.com/harness/gitness/internal/store/database" "github.com/harness/gitness/internal/store/database"
@ -130,12 +131,13 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e
triggerer.WireSet, triggerer.WireSet,
file.WireSet, file.WireSet,
runner.WireSet, runner.WireSet,
eventsstream.WireSet, sse.WireSet,
scheduler.WireSet, scheduler.WireSet,
commit.WireSet, commit.WireSet,
controllertrigger.WireSet, controllertrigger.WireSet,
plugin.WireSet, plugin.WireSet,
importer.WireSet, importer.WireSet,
canceler.WireSet,
exporter.WireSet, exporter.WireSet,
) )
return &cliserver.System{}, nil return &cliserver.System{}, nil

View File

@ -10,7 +10,7 @@ import (
"context" "context"
"github.com/harness/gitness/cli/server" "github.com/harness/gitness/cli/server"
"github.com/harness/gitness/encrypt" "github.com/harness/gitness/encrypt"
events2 "github.com/harness/gitness/events" "github.com/harness/gitness/events"
"github.com/harness/gitness/gitrpc" "github.com/harness/gitness/gitrpc"
server3 "github.com/harness/gitness/gitrpc/server" server3 "github.com/harness/gitness/gitrpc/server"
"github.com/harness/gitness/gitrpc/server/cron" "github.com/harness/gitness/gitrpc/server/cron"
@ -36,10 +36,10 @@ import (
"github.com/harness/gitness/internal/auth/authn" "github.com/harness/gitness/internal/auth/authn"
"github.com/harness/gitness/internal/auth/authz" "github.com/harness/gitness/internal/auth/authz"
"github.com/harness/gitness/internal/bootstrap" "github.com/harness/gitness/internal/bootstrap"
events4 "github.com/harness/gitness/internal/events/git" events3 "github.com/harness/gitness/internal/events/git"
events3 "github.com/harness/gitness/internal/events/pullreq" events2 "github.com/harness/gitness/internal/events/pullreq"
"github.com/harness/gitness/internal/pipeline/canceler"
"github.com/harness/gitness/internal/pipeline/commit" "github.com/harness/gitness/internal/pipeline/commit"
"github.com/harness/gitness/internal/pipeline/events"
"github.com/harness/gitness/internal/pipeline/file" "github.com/harness/gitness/internal/pipeline/file"
"github.com/harness/gitness/internal/pipeline/manager" "github.com/harness/gitness/internal/pipeline/manager"
"github.com/harness/gitness/internal/pipeline/runner" "github.com/harness/gitness/internal/pipeline/runner"
@ -55,6 +55,7 @@ import (
"github.com/harness/gitness/internal/services/pullreq" "github.com/harness/gitness/internal/services/pullreq"
trigger2 "github.com/harness/gitness/internal/services/trigger" trigger2 "github.com/harness/gitness/internal/services/trigger"
"github.com/harness/gitness/internal/services/webhook" "github.com/harness/gitness/internal/services/webhook"
"github.com/harness/gitness/internal/sse"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/internal/store/cache" "github.com/harness/gitness/internal/store/cache"
"github.com/harness/gitness/internal/store/database" "github.com/harness/gitness/internal/store/database"
@ -125,26 +126,28 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
if err != nil { if err != nil {
return nil, err return nil, err
} }
repository, err := importer.ProvideRepoImporter(config, provider, gitrpcInterface, repoStore, encrypter, jobScheduler, executor) streamer := sse.ProvideEventsStreaming(pubSub)
repository, err := importer.ProvideRepoImporter(config, provider, gitrpcInterface, repoStore, encrypter, jobScheduler, executor, streamer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
repoController := repo.ProvideController(config, db, provider, pathUID, authorizer, pathStore, repoStore, spaceStore, pipelineStore, principalStore, gitrpcInterface, repository) repoController := repo.ProvideController(config, db, provider, pathUID, authorizer, pathStore, repoStore, spaceStore, pipelineStore, principalStore, gitrpcInterface, repository)
executionStore := database.ProvideExecutionStore(db) executionStore := database.ProvideExecutionStore(db)
commitService := commit.ProvideCommitService(gitrpcInterface)
stageStore := database.ProvideStageStore(db) stageStore := database.ProvideStageStore(db)
fileService := file.ProvideFileService(gitrpcInterface)
schedulerScheduler, err := scheduler.ProvideScheduler(stageStore, mutexManager) schedulerScheduler, err := scheduler.ProvideScheduler(stageStore, mutexManager)
if err != nil { if err != nil {
return nil, err return nil, err
} }
triggererTriggerer := triggerer.ProvideTriggerer(executionStore, stageStore, db, pipelineStore, fileService, schedulerScheduler, repoStore)
executionController := execution.ProvideController(db, authorizer, executionStore, commitService, triggererTriggerer, repoStore, stageStore, pipelineStore)
stepStore := database.ProvideStepStore(db) stepStore := database.ProvideStepStore(db)
cancelerCanceler := canceler.ProvideCanceler(executionStore, streamer, repoStore, schedulerScheduler, stageStore, stepStore)
commitService := commit.ProvideCommitService(gitrpcInterface)
checkStore := database.ProvideCheckStore(db, principalInfoCache)
fileService := file.ProvideFileService(gitrpcInterface)
triggererTriggerer := triggerer.ProvideTriggerer(executionStore, checkStore, stageStore, db, pipelineStore, fileService, schedulerScheduler, repoStore)
executionController := execution.ProvideController(db, authorizer, executionStore, cancelerCanceler, commitService, triggererTriggerer, repoStore, stageStore, pipelineStore)
logStore := logs.ProvideLogStore(db, config) logStore := logs.ProvideLogStore(db, config)
logStream := livelog.ProvideLogStream(config) logStream := livelog.ProvideLogStream(config)
logsController := logs2.ProvideController(db, authorizer, executionStore, repoStore, pipelineStore, stageStore, stepStore, logStore, logStream) logsController := logs2.ProvideController(db, authorizer, executionStore, repoStore, pipelineStore, stageStore, stepStore, logStore, logStream)
eventsStreamer := events.ProvideEventsStreaming(pubSub)
secretStore := database.ProvideSecretStore(db) secretStore := database.ProvideSecretStore(db)
connectorStore := database.ProvideConnectorStore(db) connectorStore := database.ProvideConnectorStore(db)
templateStore := database.ProvideTemplateStore(db) templateStore := database.ProvideTemplateStore(db)
@ -152,7 +155,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
if err != nil { if err != nil {
return nil, err return nil, err
} }
spaceController := space.ProvideController(db, provider, eventsStreamer, pathUID, authorizer, pathStore, pipelineStore, secretStore, connectorStore, templateStore, spaceStore, repoStore, principalStore, repoController, membershipStore, repository, exporterRepository) spaceController := space.ProvideController(db, provider, streamer, pathUID, authorizer, pathStore, pipelineStore, secretStore, connectorStore, templateStore, spaceStore, repoStore, principalStore, repoController, membershipStore, repository, exporterRepository)
triggerStore := database.ProvideTriggerStore(db) triggerStore := database.ProvideTriggerStore(db)
pipelineController := pipeline.ProvideController(db, pathUID, pathStore, repoStore, triggerStore, authorizer, pipelineStore) pipelineController := pipeline.ProvideController(db, pathUID, pathStore, repoStore, triggerStore, authorizer, pipelineStore)
secretController := secret.ProvideController(db, pathUID, pathStore, encrypter, secretStore, authorizer, spaceStore) secretController := secret.ProvideController(db, pathUID, pathStore, encrypter, secretStore, authorizer, spaceStore)
@ -170,20 +173,20 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
if err != nil { if err != nil {
return nil, err return nil, err
} }
eventsSystem, err := events2.ProvideSystem(eventsConfig, universalClient) eventsSystem, err := events.ProvideSystem(eventsConfig, universalClient)
if err != nil { if err != nil {
return nil, err return nil, err
} }
reporter, err := events3.ProvideReporter(eventsSystem) reporter, err := events2.ProvideReporter(eventsSystem)
if err != nil { if err != nil {
return nil, err return nil, err
} }
migrator := codecomments.ProvideMigrator(gitrpcInterface) migrator := codecomments.ProvideMigrator(gitrpcInterface)
readerFactory, err := events4.ProvideReaderFactory(eventsSystem) readerFactory, err := events3.ProvideReaderFactory(eventsSystem)
if err != nil { if err != nil {
return nil, err return nil, err
} }
eventsReaderFactory, err := events3.ProvideReaderFactory(eventsSystem) eventsReaderFactory, err := events2.ProvideReaderFactory(eventsSystem)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -202,14 +205,13 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
return nil, err return nil, err
} }
webhookController := webhook2.ProvideController(webhookConfig, db, authorizer, webhookStore, webhookExecutionStore, repoStore, webhookService) webhookController := webhook2.ProvideController(webhookConfig, db, authorizer, webhookStore, webhookExecutionStore, repoStore, webhookService)
eventsReporter, err := events4.ProvideReporter(eventsSystem) eventsReporter, err := events3.ProvideReporter(eventsSystem)
if err != nil { if err != nil {
return nil, err return nil, err
} }
githookController := githook.ProvideController(db, authorizer, principalStore, repoStore, eventsReporter) githookController := githook.ProvideController(db, authorizer, principalStore, repoStore, eventsReporter)
serviceaccountController := serviceaccount.NewController(principalUID, authorizer, principalStore, spaceStore, repoStore, tokenStore) serviceaccountController := serviceaccount.NewController(principalUID, authorizer, principalStore, spaceStore, repoStore, tokenStore)
principalController := principal.ProvideController(principalStore) principalController := principal.ProvideController(principalStore)
checkStore := database.ProvideCheckStore(db, principalInfoCache)
checkController := check2.ProvideController(db, authorizer, repoStore, checkStore, gitrpcInterface) checkController := check2.ProvideController(db, authorizer, repoStore, checkStore, gitrpcInterface)
systemController := system.NewController(principalStore, config) systemController := system.NewController(principalStore, config)
apiHandler := router.ProvideAPIHandler(config, authenticator, repoController, executionController, logsController, spaceController, pipelineController, secretController, triggerController, connectorController, templateController, pluginController, pullreqController, webhookController, githookController, serviceaccountController, controller, principalController, checkController, systemController) apiHandler := router.ProvideAPIHandler(config, authenticator, repoController, executionController, logsController, spaceController, pipelineController, secretController, triggerController, connectorController, templateController, pluginController, pullreqController, webhookController, githookController, serviceaccountController, controller, principalController, checkController, systemController)
@ -217,7 +219,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
webHandler := router.ProvideWebHandler(config) webHandler := router.ProvideWebHandler(config)
routerRouter := router.ProvideRouter(config, apiHandler, gitHandler, webHandler) routerRouter := router.ProvideRouter(config, apiHandler, gitHandler, webHandler)
serverServer := server2.ProvideServer(config, routerRouter) serverServer := server2.ProvideServer(config, routerRouter)
executionManager := manager.ProvideExecutionManager(config, executionStore, pipelineStore, provider, eventsStreamer, fileService, logStore, logStream, repoStore, schedulerScheduler, secretStore, stageStore, stepStore, principalStore) executionManager := manager.ProvideExecutionManager(config, executionStore, pipelineStore, provider, streamer, fileService, logStore, logStream, checkStore, repoStore, schedulerScheduler, secretStore, stageStore, stepStore, principalStore)
client := manager.ProvideExecutionClient(executionManager, config) client := manager.ProvideExecutionClient(executionManager, config)
runtimeRunner, err := runner.ProvideExecutionRunner(config, client, executionManager) runtimeRunner, err := runner.ProvideExecutionRunner(config, client, executionManager)
if err != nil { if err != nil {

View File

@ -74,6 +74,10 @@ func (in *ReportInput) Validate() error {
} }
in.Payload.Data = payloadDataJSON in.Payload.Data = payloadDataJSON
case enum.CheckPayloadKindPipeline:
return usererror.BadRequest("Kind cannot be pipeline for external checks")
} }
return nil return nil

View File

@ -14,23 +14,18 @@ import (
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
) )
type UpdateInput struct { func (c *Controller) Cancel(
Status string `json:"status"`
}
func (c *Controller) Update(
ctx context.Context, ctx context.Context,
session *auth.Session, session *auth.Session,
repoRef string, repoRef string,
pipelineUID string, pipelineUID string,
executionNum int64, executionNum int64,
in *UpdateInput) (*types.Execution, error) { ) (*types.Execution, error) {
repo, err := c.repoStore.FindByRef(ctx, repoRef) repo, err := c.repoStore.FindByRef(ctx, repoRef)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to find repo by ref: %w", err) return nil, fmt.Errorf("failed to find repo by ref: %w", err)
} }
err = apiauth.CheckPipeline(ctx, c.authorizer, session, repo.Path, pipelineUID, enum.PermissionPipelineExecute)
err = apiauth.CheckPipeline(ctx, c.authorizer, session, repo.Path, pipelineUID, enum.PermissionPipelineEdit)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to authorize: %w", err) return nil, fmt.Errorf("failed to authorize: %w", err)
} }
@ -42,16 +37,13 @@ func (c *Controller) Update(
execution, err := c.executionStore.FindByNumber(ctx, pipeline.ID, executionNum) execution, err := c.executionStore.FindByNumber(ctx, pipeline.ID, executionNum)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to find execution: %w", err) return nil, fmt.Errorf("failed to find execution %d: %w", executionNum, err)
} }
return c.executionStore.UpdateOptLock(ctx, err = c.canceler.Cancel(ctx, repo, execution)
execution, func(original *types.Execution) error { if err != nil {
// update values only if provided return nil, fmt.Errorf("unable to cancel execution: %w", err)
if in.Status != "" {
original.Status = in.Status
} }
return nil return execution, nil
})
} }

View File

@ -6,6 +6,7 @@ package execution
import ( import (
"github.com/harness/gitness/internal/auth/authz" "github.com/harness/gitness/internal/auth/authz"
"github.com/harness/gitness/internal/pipeline/canceler"
"github.com/harness/gitness/internal/pipeline/commit" "github.com/harness/gitness/internal/pipeline/commit"
"github.com/harness/gitness/internal/pipeline/triggerer" "github.com/harness/gitness/internal/pipeline/triggerer"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
@ -17,6 +18,7 @@ type Controller struct {
db *sqlx.DB db *sqlx.DB
authorizer authz.Authorizer authorizer authz.Authorizer
executionStore store.ExecutionStore executionStore store.ExecutionStore
canceler canceler.Canceler
commitService commit.CommitService commitService commit.CommitService
triggerer triggerer.Triggerer triggerer triggerer.Triggerer
repoStore store.RepoStore repoStore store.RepoStore
@ -28,6 +30,7 @@ func NewController(
db *sqlx.DB, db *sqlx.DB,
authorizer authz.Authorizer, authorizer authz.Authorizer,
executionStore store.ExecutionStore, executionStore store.ExecutionStore,
canceler canceler.Canceler,
commitService commit.CommitService, commitService commit.CommitService,
triggerer triggerer.Triggerer, triggerer triggerer.Triggerer,
repoStore store.RepoStore, repoStore store.RepoStore,
@ -38,6 +41,7 @@ func NewController(
db: db, db: db,
authorizer: authorizer, authorizer: authorizer,
executionStore: executionStore, executionStore: executionStore,
canceler: canceler,
commitService: commitService, commitService: commitService,
triggerer: triggerer, triggerer: triggerer,
repoStore: repoStore, repoStore: repoStore,

View File

@ -60,6 +60,7 @@ func (c *Controller) Create(
hook := &triggerer.Hook{ hook := &triggerer.Hook{
Trigger: session.Principal.UID, // who/what triggered the build, different from commit author Trigger: session.Principal.UID, // who/what triggered the build, different from commit author
AuthorLogin: commit.Author.Identity.Name, AuthorLogin: commit.Author.Identity.Name,
TriggeredBy: session.Principal.ID,
AuthorName: commit.Author.Identity.Name, AuthorName: commit.Author.Identity.Name,
AuthorEmail: commit.Author.Identity.Email, AuthorEmail: commit.Author.Identity.Email,
Ref: ref, Ref: ref,

View File

@ -6,6 +6,7 @@ package execution
import ( import (
"github.com/harness/gitness/internal/auth/authz" "github.com/harness/gitness/internal/auth/authz"
"github.com/harness/gitness/internal/pipeline/canceler"
"github.com/harness/gitness/internal/pipeline/commit" "github.com/harness/gitness/internal/pipeline/commit"
"github.com/harness/gitness/internal/pipeline/triggerer" "github.com/harness/gitness/internal/pipeline/triggerer"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
@ -22,12 +23,13 @@ var WireSet = wire.NewSet(
func ProvideController(db *sqlx.DB, func ProvideController(db *sqlx.DB,
authorizer authz.Authorizer, authorizer authz.Authorizer,
executionStore store.ExecutionStore, executionStore store.ExecutionStore,
canceler canceler.Canceler,
commitService commit.CommitService, commitService commit.CommitService,
triggerer triggerer.Triggerer, triggerer triggerer.Triggerer,
repoStore store.RepoStore, repoStore store.RepoStore,
stageStore store.StageStore, stageStore store.StageStore,
pipelineStore store.PipelineStore, pipelineStore store.PipelineStore,
) *Controller { ) *Controller {
return NewController(db, authorizer, executionStore, commitService, return NewController(db, authorizer, executionStore, canceler, commitService,
triggerer, repoStore, stageStore, pipelineStore) triggerer, repoStore, stageStore, pipelineStore)
} }

View File

@ -59,6 +59,7 @@ func (c *Controller) Create(
Description: in.Description, Description: in.Description,
RepoID: repo.ID, RepoID: repo.ID,
UID: in.UID, UID: in.UID,
CreatedBy: session.Principal.ID,
Seq: 0, Seq: 0,
DefaultBranch: in.DefaultBranch, DefaultBranch: in.DefaultBranch,
ConfigPath: in.ConfigPath, ConfigPath: in.ConfigPath,
@ -84,7 +85,7 @@ func (c *Controller) Create(
UID: "default", UID: "default",
Actions: []enum.TriggerAction{enum.TriggerActionPullReqCreated, Actions: []enum.TriggerAction{enum.TriggerActionPullReqCreated,
enum.TriggerActionPullReqReopened, enum.TriggerActionPullReqBranchUpdated}, enum.TriggerActionPullReqReopened, enum.TriggerActionPullReqBranchUpdated},
Enabled: true, Disabled: false,
Version: 0, Version: 0,
} }
err = c.triggerStore.Create(ctx, trigger) err = c.triggerStore.Create(ctx, trigger)

View File

@ -108,7 +108,7 @@ func (c *Controller) GetContent(ctx context.Context,
IncludeLatestCommit: includeLatestCommit, IncludeLatestCommit: includeLatestCommit,
}) })
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to read tree node: %w", err)
} }
info, err := mapToContentInfo(treeNodeOutput.Node, treeNodeOutput.Commit, includeLatestCommit) info, err := mapToContentInfo(treeNodeOutput.Node, treeNodeOutput.Commit, includeLatestCommit)

View File

@ -40,10 +40,7 @@ func (c *Controller) Delete(ctx context.Context, session *auth.Session, repoRef
log.Ctx(ctx).Info().Msgf("Delete request received for repo %s , id: %d", repo.Path, repo.ID) log.Ctx(ctx).Info().Msgf("Delete request received for repo %s , id: %d", repo.Path, repo.ID)
// TODO: uncomment when soft delete is implemented return c.DeleteNoAuth(ctx, session, repo)
// return c.DeleteNoAuth(ctx, session, repo)
return nil
} }
func (c *Controller) DeleteNoAuth(ctx context.Context, session *auth.Session, repo *types.Repository) error { func (c *Controller) DeleteNoAuth(ctx context.Context, session *auth.Session, repo *types.Repository) error {

View File

@ -7,11 +7,11 @@ package repo
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/harness/gitness/internal/api/usererror" "github.com/harness/gitness/internal/api/usererror"
"github.com/harness/gitness/internal/auth" "github.com/harness/gitness/internal/auth"
"github.com/harness/gitness/internal/paths" "github.com/harness/gitness/internal/paths"
"github.com/harness/gitness/internal/services/importer" "github.com/harness/gitness/internal/services/importer"
"github.com/harness/gitness/internal/services/job"
"github.com/harness/gitness/store/database/dbtx" "github.com/harness/gitness/store/database/dbtx"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
@ -43,11 +43,6 @@ func (c *Controller) Import(ctx context.Context, session *auth.Session, in *Impo
return nil, err return nil, err
} }
jobUID, err := job.UID()
if err != nil {
return nil, fmt.Errorf("error creating job UID: %w", err)
}
var repo *types.Repository var repo *types.Repository
err = dbtx.New(c.db).WithTx(ctx, func(ctx context.Context) error { err = dbtx.New(c.db).WithTx(ctx, func(ctx context.Context) error {
// lock parent space path to ensure it doesn't get updated while we setup new repo // lock parent space path to ensure it doesn't get updated while we setup new repo
@ -57,7 +52,7 @@ func (c *Controller) Import(ctx context.Context, session *auth.Session, in *Impo
} }
pathToRepo := paths.Concatinate(spacePath.Value, in.UID) pathToRepo := paths.Concatinate(spacePath.Value, in.UID)
repo = remoteRepository.ToRepo(parentSpace.ID, pathToRepo, in.UID, in.Description, jobUID, &session.Principal) repo = remoteRepository.ToRepo(parentSpace.ID, pathToRepo, in.UID, in.Description, &session.Principal)
err = c.repoStore.Create(ctx, repo) err = c.repoStore.Create(ctx, repo)
if err != nil { if err != nil {

View File

@ -42,7 +42,7 @@ func (c *Controller) Raw(ctx context.Context,
IncludeLatestCommit: false, IncludeLatestCommit: false,
}) })
if err != nil { if err != nil {
return nil, 0, err return nil, 0, fmt.Errorf("failed to read tree node: %w", err)
} }
// viewing Raw content is only supported for blob content // viewing Raw content is only supported for blob content

View File

@ -51,6 +51,7 @@ func (c *Controller) Create(ctx context.Context, session *auth.Session, in *Crea
var secret *types.Secret var secret *types.Secret
now := time.Now().UnixMilli() now := time.Now().UnixMilli()
secret = &types.Secret{ secret = &types.Secret{
CreatedBy: session.Principal.ID,
Description: in.Description, Description: in.Description,
Data: in.Data, Data: in.Data,
SpaceID: parentSpace.ID, SpaceID: parentSpace.ID,

View File

@ -7,9 +7,9 @@ package space
import ( import (
"github.com/harness/gitness/internal/api/controller/repo" "github.com/harness/gitness/internal/api/controller/repo"
"github.com/harness/gitness/internal/auth/authz" "github.com/harness/gitness/internal/auth/authz"
"github.com/harness/gitness/internal/pipeline/events"
"github.com/harness/gitness/internal/services/exporter" "github.com/harness/gitness/internal/services/exporter"
"github.com/harness/gitness/internal/services/importer" "github.com/harness/gitness/internal/services/importer"
"github.com/harness/gitness/internal/sse"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/internal/url" "github.com/harness/gitness/internal/url"
"github.com/harness/gitness/types/check" "github.com/harness/gitness/types/check"
@ -20,7 +20,7 @@ import (
type Controller struct { type Controller struct {
db *sqlx.DB db *sqlx.DB
urlProvider *url.Provider urlProvider *url.Provider
eventsStream events.EventsStreamer sseStreamer sse.Streamer
uidCheck check.PathUID uidCheck check.PathUID
authorizer authz.Authorizer authorizer authz.Authorizer
pathStore store.PathStore pathStore store.PathStore
@ -37,7 +37,7 @@ type Controller struct {
exporter *exporter.Repository exporter *exporter.Repository
} }
func NewController(db *sqlx.DB, urlProvider *url.Provider, eventsStream events.EventsStreamer, func NewController(db *sqlx.DB, urlProvider *url.Provider, sseStreamer sse.Streamer,
uidCheck check.PathUID, authorizer authz.Authorizer, uidCheck check.PathUID, authorizer authz.Authorizer,
pathStore store.PathStore, pipelineStore store.PipelineStore, secretStore store.SecretStore, pathStore store.PathStore, pipelineStore store.PipelineStore, secretStore store.SecretStore,
connectorStore store.ConnectorStore, templateStore store.TemplateStore, spaceStore store.SpaceStore, connectorStore store.ConnectorStore, templateStore store.TemplateStore, spaceStore store.SpaceStore,
@ -47,7 +47,7 @@ func NewController(db *sqlx.DB, urlProvider *url.Provider, eventsStream events.E
return &Controller{ return &Controller{
db: db, db: db,
urlProvider: urlProvider, urlProvider: urlProvider,
eventsStream: eventsStream, sseStreamer: sseStreamer,
uidCheck: uidCheck, uidCheck: uidCheck,
authorizer: authorizer, authorizer: authorizer,
pathStore: pathStore, pathStore: pathStore,

View File

@ -13,8 +13,6 @@ import (
"github.com/harness/gitness/internal/auth" "github.com/harness/gitness/internal/auth"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
"github.com/rs/zerolog/log"
) )
// Delete deletes a space. // Delete deletes a space.
@ -26,13 +24,12 @@ func (c *Controller) Delete(ctx context.Context, session *auth.Session, spaceRef
if err = apiauth.CheckSpace(ctx, c.authorizer, session, space, enum.PermissionSpaceDelete, false); err != nil { if err = apiauth.CheckSpace(ctx, c.authorizer, session, space, enum.PermissionSpaceDelete, false); err != nil {
return err return err
} }
// TODO: uncomment when soft delete is implemented
log.Ctx(ctx).Info().Msgf("Delete request received for space %s", space.Path) return c.DeleteNoAuth(ctx, session, space.ID)
// return c.DeleteNoAuth(ctx, session, space.ID)
return nil
} }
// DeleteNoAuth bypasses PermissionSpaceDelete, PermissionSpaceView, PermissionRepoView, and PermissionRepoDelete. // DeleteNoAuth deletes the space - no authorization is verified.
// WARNING this is meant for internal calls only.
func (c *Controller) DeleteNoAuth(ctx context.Context, session *auth.Session, spaceID int64) error { func (c *Controller) DeleteNoAuth(ctx context.Context, session *auth.Session, spaceID int64) error {
filter := &types.SpaceFilter{ filter := &types.SpaceFilter{
Page: 1, Page: 1,
@ -62,10 +59,25 @@ func (c *Controller) DeleteNoAuth(ctx context.Context, session *auth.Session, sp
return nil return nil
} }
func (c *Controller) DeleteWithPathNoAuth(ctx context.Context, session *auth.Session, spacePath string) error { // deleteRepositoriesNoAuth deletes all repositories in a space - no authorization is verified.
space, err := c.spaceStore.FindByRef(ctx, spacePath) // WARNING this is meant for internal calls only.
if err != nil { func (c *Controller) deleteRepositoriesNoAuth(ctx context.Context, session *auth.Session, spaceID int64) error {
return err filter := &types.RepoFilter{
Page: 1,
Size: int(math.MaxInt),
Query: "",
Order: enum.OrderAsc,
Sort: enum.RepoAttrNone,
} }
return c.DeleteNoAuth(ctx, session, space.ID) repos, _, err := c.ListRepositoriesNoAuth(ctx, spaceID, filter)
if err != nil {
return fmt.Errorf("failed to list space repositories: %w", err)
}
for _, repo := range repos {
err = c.repoCtrl.DeleteNoAuth(ctx, session, repo)
if err != nil {
return fmt.Errorf("failed to delete repository %d: %w", repo.ID, err)
}
}
return nil
} }

View File

@ -1,38 +0,0 @@
// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
package space
import (
"context"
"fmt"
"math"
"github.com/harness/gitness/internal/auth"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
)
// deleteRepositoriesNoAuth does not check PermissionRepoView, and PermissionRepoDelete permissions.
// Call this through Delete(Space) api to make sure the caller has DeleteSpace permission.
func (c *Controller) deleteRepositoriesNoAuth(ctx context.Context, session *auth.Session, spaceID int64) error {
filter := &types.RepoFilter{
Page: 1,
Size: int(math.MaxInt),
Query: "",
Order: enum.OrderAsc,
Sort: enum.RepoAttrNone,
}
repos, _, err := c.ListRepositoriesNoAuth(ctx, spaceID, filter)
if err != nil {
return fmt.Errorf("failed to list space repositories: %w", err)
}
for _, repo := range repos {
err = c.repoCtrl.DeleteNoAuth(ctx, session, repo)
if err != nil {
return fmt.Errorf("failed to delete repository %d: %w", repo.ID, err)
}
}
return nil
}

View File

@ -39,16 +39,21 @@ func (c *Controller) Events(
return fmt.Errorf("failed to authorize stream: %w", err) return fmt.Errorf("failed to authorize stream: %w", err)
} }
ctx, cancel := context.WithTimeout(ctx, tailMaxTime) ctx, ctxCancel := context.WithTimeout(ctx, tailMaxTime)
defer cancel() defer ctxCancel()
io.WriteString(w, ": ping\n\n") io.WriteString(w, ": ping\n\n")
w.Flush() w.Flush()
events, errc, consumer := c.eventsStream.Subscribe(ctx, space.ID) eventStream, errorStream, sseCancel := c.sseStreamer.Stream(ctx, space.ID)
defer c.eventsStream.Unsubscribe(ctx, consumer) defer func() {
uerr := sseCancel(ctx)
if uerr != nil {
log.Ctx(ctx).Warn().Err(uerr).Msgf("failed to cancel sse stream for space '%s'", space.Path)
}
}()
// could not get error channel // could not get error channel
if errc == nil { if errorStream == nil {
io.WriteString(w, "event: error\ndata: eof\n\n") io.WriteString(w, "event: error\ndata: eof\n\n")
w.Flush() w.Flush()
return fmt.Errorf("could not get error channel") return fmt.Errorf("could not get error channel")
@ -72,17 +77,20 @@ L:
case <-ctx.Done(): case <-ctx.Done():
log.Debug().Msg("events: stream cancelled") log.Debug().Msg("events: stream cancelled")
break L break L
case err := <-errc: case err := <-errorStream:
log.Err(err).Msg("events: received error in the tail channel") log.Err(err).Msg("events: received error in the tail channel")
break L break L
case <-pingTimer.C: case <-pingTimer.C:
// if time b/w messages takes longer, send a ping // if time b/w messages takes longer, send a ping
io.WriteString(w, ": ping\n\n") io.WriteString(w, ": ping\n\n")
w.Flush() w.Flush()
case event := <-events: case event := <-eventStream:
io.WriteString(w, fmt.Sprintf("event: %s\n", event.Type))
io.WriteString(w, "data: ") io.WriteString(w, "data: ")
enc.Encode(event) enc.Encode(event.Data)
io.WriteString(w, "\n\n") // NOTE: enc.Encode is ending the data with a new line, only add one more
// Source: https://cs.opensource.google/go/go/+/refs/tags/go1.21.1:src/encoding/json/stream.go;l=220
io.WriteString(w, "\n")
w.Flush() w.Flush()
} }
} }

View File

@ -16,7 +16,6 @@ import (
"github.com/harness/gitness/internal/bootstrap" "github.com/harness/gitness/internal/bootstrap"
"github.com/harness/gitness/internal/paths" "github.com/harness/gitness/internal/paths"
"github.com/harness/gitness/internal/services/importer" "github.com/harness/gitness/internal/services/importer"
"github.com/harness/gitness/internal/services/job"
"github.com/harness/gitness/store/database/dbtx" "github.com/harness/gitness/store/database/dbtx"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/check" "github.com/harness/gitness/types/check"
@ -131,16 +130,9 @@ func (c *Controller) Import(ctx context.Context, session *auth.Session, in *Impo
} }
for i, remoteRepository := range remoteRepositories { for i, remoteRepository := range remoteRepositories {
var jobUID string
jobUID, err = job.UID()
if err != nil {
return fmt.Errorf("error creating job UID: %w", err)
}
pathToRepo := paths.Concatinate(path.Value, remoteRepository.UID) pathToRepo := paths.Concatinate(path.Value, remoteRepository.UID)
repo := remoteRepository.ToRepo( repo := remoteRepository.ToRepo(
space.ID, pathToRepo, remoteRepository.UID, "", jobUID, &session.Principal) space.ID, pathToRepo, remoteRepository.UID, "", &session.Principal)
err = c.repoStore.Create(ctx, repo) err = c.repoStore.Create(ctx, repo)
if err != nil { if err != nil {

View File

@ -7,9 +7,9 @@ package space
import ( import (
"github.com/harness/gitness/internal/api/controller/repo" "github.com/harness/gitness/internal/api/controller/repo"
"github.com/harness/gitness/internal/auth/authz" "github.com/harness/gitness/internal/auth/authz"
"github.com/harness/gitness/internal/pipeline/events"
"github.com/harness/gitness/internal/services/exporter" "github.com/harness/gitness/internal/services/exporter"
"github.com/harness/gitness/internal/services/importer" "github.com/harness/gitness/internal/services/importer"
"github.com/harness/gitness/internal/sse"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/internal/url" "github.com/harness/gitness/internal/url"
"github.com/harness/gitness/types/check" "github.com/harness/gitness/types/check"
@ -23,14 +23,14 @@ var WireSet = wire.NewSet(
ProvideController, ProvideController,
) )
func ProvideController(db *sqlx.DB, urlProvider *url.Provider, eventsStream events.EventsStreamer, func ProvideController(db *sqlx.DB, urlProvider *url.Provider, sseStreamer sse.Streamer,
uidCheck check.PathUID, authorizer authz.Authorizer, pathStore store.PathStore, uidCheck check.PathUID, authorizer authz.Authorizer, pathStore store.PathStore,
pipelineStore store.PipelineStore, secretStore store.SecretStore, pipelineStore store.PipelineStore, secretStore store.SecretStore,
connectorStore store.ConnectorStore, templateStore store.TemplateStore, connectorStore store.ConnectorStore, templateStore store.TemplateStore,
spaceStore store.SpaceStore, repoStore store.RepoStore, principalStore store.PrincipalStore, spaceStore store.SpaceStore, repoStore store.RepoStore, principalStore store.PrincipalStore,
repoCtrl *repo.Controller, membershipStore store.MembershipStore, importer *importer.Repository, exporter *exporter.Repository, repoCtrl *repo.Controller, membershipStore store.MembershipStore, importer *importer.Repository, exporter *exporter.Repository,
) *Controller { ) *Controller {
return NewController(db, urlProvider, eventsStream, uidCheck, authorizer, return NewController(db, urlProvider, sseStreamer, uidCheck, authorizer,
pathStore, pipelineStore, secretStore, pathStore, pipelineStore, secretStore,
connectorStore, templateStore, connectorStore, templateStore,
spaceStore, repoStore, principalStore, spaceStore, repoStore, principalStore,

View File

@ -21,7 +21,7 @@ type CreateInput struct {
Description string `json:"description"` Description string `json:"description"`
UID string `json:"uid"` UID string `json:"uid"`
Secret string `json:"secret"` Secret string `json:"secret"`
Enabled bool `json:"enabled"` Disabled bool `json:"disabled"`
Actions []enum.TriggerAction `json:"actions"` Actions []enum.TriggerAction `json:"actions"`
} }
@ -56,7 +56,7 @@ func (c *Controller) Create(
now := time.Now().UnixMilli() now := time.Now().UnixMilli()
trigger := &types.Trigger{ trigger := &types.Trigger{
Description: in.Description, Description: in.Description,
Enabled: in.Enabled, Disabled: in.Disabled,
Secret: in.Secret, Secret: in.Secret,
CreatedBy: session.Principal.ID, CreatedBy: session.Principal.ID,
RepoID: repo.ID, RepoID: repo.ID,

View File

@ -21,7 +21,7 @@ type UpdateInput struct {
UID *string `json:"uid"` UID *string `json:"uid"`
Actions []enum.TriggerAction `json:"actions"` Actions []enum.TriggerAction `json:"actions"`
Secret *string `json:"secret"` Secret *string `json:"secret"`
Enabled *bool `json:"enabled"` // can be nil, so keeping it a pointer Disabled *bool `json:"disabled"` // can be nil, so keeping it a pointer
} }
func (c *Controller) Update( func (c *Controller) Update(
@ -72,8 +72,8 @@ func (c *Controller) Update(
if in.Secret != nil { if in.Secret != nil {
original.Secret = *in.Secret original.Secret = *in.Secret
} }
if in.Enabled != nil { if in.Disabled != nil {
original.Enabled = *in.Enabled original.Disabled = *in.Disabled
} }
return nil return nil

View File

@ -1,11 +1,9 @@
// Copyright 2022 Harness Inc. All rights reserved. // Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License // Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository. // that can be found in the LICENSE.md file for this repository.
package execution package execution
import ( import (
"encoding/json"
"net/http" "net/http"
"github.com/harness/gitness/internal/api/controller/execution" "github.com/harness/gitness/internal/api/controller/execution"
@ -13,40 +11,32 @@ import (
"github.com/harness/gitness/internal/api/request" "github.com/harness/gitness/internal/api/request"
) )
func HandleUpdate(executionCtrl *execution.Controller) http.HandlerFunc { func HandleCancel(executionCtrl *execution.Controller) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
session, _ := request.AuthSessionFrom(ctx) session, _ := request.AuthSessionFrom(ctx)
in := new(execution.UpdateInput)
err := json.NewDecoder(r.Body).Decode(in)
if err != nil {
render.BadRequestf(w, "Invalid Request Body: %s.", err)
return
}
pipelineUID, err := request.GetPipelineUIDFromPath(r) pipelineUID, err := request.GetPipelineUIDFromPath(r)
if err != nil { if err != nil {
render.TranslatedUserError(w, err) render.TranslatedUserError(w, err)
return return
} }
repoRef, err := request.GetRepoRefFromPath(r)
if err != nil {
render.TranslatedUserError(w, err)
return
}
n, err := request.GetExecutionNumberFromPath(r) n, err := request.GetExecutionNumberFromPath(r)
if err != nil { if err != nil {
render.TranslatedUserError(w, err) render.TranslatedUserError(w, err)
return return
} }
repoRef, err := request.GetRepoRefFromPath(r)
pipeline, err := executionCtrl.Update(ctx, session, repoRef, pipelineUID, n, in)
if err != nil { if err != nil {
render.TranslatedUserError(w, err) render.TranslatedUserError(w, err)
return return
} }
render.JSON(w, http.StatusOK, pipeline) execution, err := executionCtrl.Cancel(ctx, session, repoRef, pipelineUID, n)
if err != nil {
render.TranslatedUserError(w, err)
return
}
render.JSON(w, http.StatusOK, execution)
} }
} }

View File

@ -15,9 +15,9 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
// HandleEventsStream returns an http.HandlerFunc that watches for // HandleEvents returns an http.HandlerFunc that watches for
// events on a space // events on a space
func HandleEventsStream(spaceCtrl *space.Controller) http.HandlerFunc { func HandleEvents(spaceCtrl *space.Controller) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
session, _ := request.AuthSessionFrom(ctx) session, _ := request.AuthSessionFrom(ctx)

View File

@ -7,7 +7,6 @@ package openapi
import ( import (
"net/http" "net/http"
"github.com/harness/gitness/internal/api/controller/execution"
"github.com/harness/gitness/internal/api/controller/pipeline" "github.com/harness/gitness/internal/api/controller/pipeline"
"github.com/harness/gitness/internal/api/controller/trigger" "github.com/harness/gitness/internal/api/controller/trigger"
"github.com/harness/gitness/internal/api/request" "github.com/harness/gitness/internal/api/request"
@ -66,11 +65,6 @@ type getPipelineRequest struct {
pipelineRequest pipelineRequest
} }
type updateExecutionRequest struct {
executionRequest
execution.UpdateInput
}
type updateTriggerRequest struct { type updateTriggerRequest struct {
triggerRequest triggerRequest
trigger.UpdateInput trigger.UpdateInput
@ -193,6 +187,18 @@ func pipelineOperations(reflector *openapi3.Reflector) {
_ = reflector.Spec.AddOperation(http.MethodGet, _ = reflector.Spec.AddOperation(http.MethodGet,
"/repos/{repo_ref}/pipelines/{pipeline_uid}/executions/{execution_number}", executionFind) "/repos/{repo_ref}/pipelines/{pipeline_uid}/executions/{execution_number}", executionFind)
executionCancel := openapi3.Operation{}
executionCancel.WithTags("pipeline")
executionCancel.WithMapOfAnything(map[string]interface{}{"operationId": "cancelExecution"})
_ = reflector.SetRequest(&executionCancel, new(getExecutionRequest), http.MethodPost)
_ = reflector.SetJSONResponse(&executionCancel, new(types.Execution), http.StatusOK)
_ = reflector.SetJSONResponse(&executionCancel, new(usererror.Error), http.StatusInternalServerError)
_ = reflector.SetJSONResponse(&executionCancel, new(usererror.Error), http.StatusUnauthorized)
_ = reflector.SetJSONResponse(&executionCancel, new(usererror.Error), http.StatusForbidden)
_ = reflector.SetJSONResponse(&executionCancel, new(usererror.Error), http.StatusNotFound)
_ = reflector.Spec.AddOperation(http.MethodPost,
"/repos/{repo_ref}/pipelines/{pipeline_uid}/executions/{execution_number}/cancel", executionCancel)
executionDelete := openapi3.Operation{} executionDelete := openapi3.Operation{}
executionDelete.WithTags("pipeline") executionDelete.WithTags("pipeline")
executionDelete.WithMapOfAnything(map[string]interface{}{"operationId": "deleteExecution"}) executionDelete.WithMapOfAnything(map[string]interface{}{"operationId": "deleteExecution"})
@ -205,19 +211,6 @@ func pipelineOperations(reflector *openapi3.Reflector) {
_ = reflector.Spec.AddOperation(http.MethodDelete, _ = reflector.Spec.AddOperation(http.MethodDelete,
"/repos/{repo_ref}/pipelines/{pipeline_uid}/executions/{execution_number}", executionDelete) "/repos/{repo_ref}/pipelines/{pipeline_uid}/executions/{execution_number}", executionDelete)
executionUpdate := openapi3.Operation{}
executionUpdate.WithTags("pipeline")
executionUpdate.WithMapOfAnything(map[string]interface{}{"operationId": "updateExecution"})
_ = reflector.SetRequest(&executionUpdate, new(updateExecutionRequest), http.MethodPatch)
_ = reflector.SetJSONResponse(&executionUpdate, new(types.Execution), http.StatusOK)
_ = reflector.SetJSONResponse(&executionUpdate, new(usererror.Error), http.StatusBadRequest)
_ = reflector.SetJSONResponse(&executionUpdate, new(usererror.Error), http.StatusInternalServerError)
_ = reflector.SetJSONResponse(&executionUpdate, new(usererror.Error), http.StatusUnauthorized)
_ = reflector.SetJSONResponse(&executionUpdate, new(usererror.Error), http.StatusForbidden)
_ = reflector.SetJSONResponse(&executionUpdate, new(usererror.Error), http.StatusNotFound)
_ = reflector.Spec.AddOperation(http.MethodPatch,
"/repos/{repo_ref}/pipelines/{pipeline_uid}/executions/{execution_number}", executionUpdate)
executionList := openapi3.Operation{} executionList := openapi3.Operation{}
executionList.WithTags("pipeline") executionList.WithTags("pipeline")
executionList.WithMapOfAnything(map[string]interface{}{"operationId": "listExecutions"}) executionList.WithMapOfAnything(map[string]interface{}{"operationId": "listExecutions"})

View File

@ -0,0 +1,143 @@
package canceler
import (
"context"
"fmt"
"time"
"github.com/harness/gitness/internal/pipeline/scheduler"
"github.com/harness/gitness/internal/sse"
"github.com/harness/gitness/internal/store"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
"github.com/rs/zerolog/log"
)
type service struct {
executionStore store.ExecutionStore
sseStreamer sse.Streamer
repoStore store.RepoStore
scheduler scheduler.Scheduler
stageStore store.StageStore
stepStore store.StepStore
}
// Canceler cancels a build.
type Canceler interface {
// Cancel cancels the provided execution.
Cancel(ctx context.Context, repo *types.Repository, execution *types.Execution) error
}
// New returns a cancellation service that encapsulates
// all cancellation operations.
func New(
executionStore store.ExecutionStore,
sseStreamer sse.Streamer,
repoStore store.RepoStore,
scheduler scheduler.Scheduler,
stageStore store.StageStore,
stepStore store.StepStore,
) Canceler {
return &service{
executionStore: executionStore,
sseStreamer: sseStreamer,
repoStore: repoStore,
scheduler: scheduler,
stageStore: stageStore,
stepStore: stepStore,
}
}
func (s *service) Cancel(ctx context.Context, repo *types.Repository, execution *types.Execution) error {
log := log.With().
Int64("execution.id", execution.ID).
Str("execution.status", string(execution.Status)).
Str("execution.Ref", execution.Ref).
Logger()
// do not cancel the build if the build status is
// complete. only cancel the build if the status is
// running or pending.
switch execution.Status {
case enum.CIStatusPending, enum.CIStatusRunning:
default:
return nil
}
// update the build status to killed. if the update fails
// due to an optimistic lock error it means the build has
// already started, and should now be ignored.
now := time.Now().UnixMilli()
execution.Status = enum.CIStatusKilled
execution.Finished = now
if execution.Started == 0 {
execution.Started = now
}
err := s.executionStore.Update(ctx, execution)
if err != nil {
return fmt.Errorf("could not update execution status to canceled: %w", err)
}
stages, err := s.stageStore.ListWithSteps(ctx, execution.ID)
if err != nil {
return fmt.Errorf("could not list stages with steps: %w", err)
}
// update the status of all steps to indicate they
// were killed or skipped.
for _, stage := range stages {
if stage.Status.IsDone() {
continue
}
if stage.Started != 0 {
stage.Status = enum.CIStatusKilled
} else {
stage.Status = enum.CIStatusSkipped
stage.Started = now
}
stage.Stopped = now
err := s.stageStore.Update(ctx, stage)
if err != nil {
log.Debug().Err(err).
Int64("stage.number", stage.Number).
Msg("canceler: cannot update stage status")
}
// update the status of all steps to indicate they
// were killed or skipped.
for _, step := range stage.Steps {
if step.Status.IsDone() {
continue
}
if step.Started != 0 {
step.Status = enum.CIStatusKilled
} else {
step.Status = enum.CIStatusSkipped
step.Started = now
}
step.Stopped = now
step.ExitCode = 130
err := s.stepStore.Update(ctx, step)
if err != nil {
log.Debug().Err(err).
Int64("stage.number", stage.Number).
Int64("step.number", step.Number).
Msg("canceler: cannot update step status")
}
}
}
execution.Stages = stages
log.Info().Msg("canceler: successfully cancelled build")
// trigger a SSE to notify subscribers that
// the execution was cancelled.
err = s.sseStreamer.Publish(ctx, repo.ParentID, enum.SSETypeExecutionCanceled, execution)
if err != nil {
log.Debug().Err(err).Msg("canceler: failed to publish server-sent event")
}
return nil
}

View File

@ -0,0 +1,29 @@
// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
package canceler
import (
"github.com/harness/gitness/internal/pipeline/scheduler"
"github.com/harness/gitness/internal/sse"
"github.com/harness/gitness/internal/store"
"github.com/google/wire"
)
// WireSet provides a wire set for this package.
var WireSet = wire.NewSet(
ProvideCanceler,
)
// ProvideExecutionManager provides an execution manager.
func ProvideCanceler(
executionStore store.ExecutionStore,
sseStreamer sse.Streamer,
repoStore store.RepoStore,
scheduler scheduler.Scheduler,
stageStore store.StageStore,
stepStore store.StepStore) Canceler {
return New(executionStore, sseStreamer, repoStore, scheduler, stageStore, stepStore)
}

View File

@ -0,0 +1,57 @@
package checks
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/harness/gitness/internal/store"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
)
// Write is a util function which writes execution and pipeline state to the
// check store.
func Write(
ctx context.Context,
checkStore store.CheckStore,
execution *types.Execution,
pipeline *types.Pipeline,
) error {
payload := types.CheckPayloadInternal{
Number: execution.Number,
RepoID: execution.RepoID,
PipelineID: execution.PipelineID,
}
data, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("could not marshal check payload: %w", err)
}
now := time.Now().UnixMilli()
summary := pipeline.Description
if summary == "" {
summary = pipeline.UID
}
check := &types.Check{
RepoID: execution.RepoID,
UID: pipeline.UID,
Summary: summary,
Created: now,
Updated: now,
CreatedBy: execution.CreatedBy,
Status: execution.Status.ConvertToCheckStatus(),
CommitSHA: execution.After,
Metadata: []byte("{}"),
Payload: types.CheckPayload{
Version: "1",
Kind: enum.CheckPayloadKindPipeline,
Data: data,
},
}
err = checkStore.Upsert(ctx, check)
if err != nil {
return fmt.Errorf("could not upsert to check store: %w", err)
}
return nil
}

View File

@ -1,83 +0,0 @@
// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
package events
import (
"context"
"encoding/json"
"strconv"
"github.com/harness/gitness/pubsub"
"github.com/harness/gitness/types/enum"
)
// Event is an event which is sent to the UI via server-sent events.
type Event struct {
Type enum.EventType `json:"type"`
Data json.RawMessage `json:"data"`
}
type EventsStreamer interface {
// Publish publishes an event to a given space ID.
Publish(ctx context.Context, spaceID int64, event *Event) error
// Subscribe listens to events on a space ID.
Subscribe(ctx context.Context, spaceID int64) (<-chan *Event, <-chan error, pubsub.Consumer)
// Unsubscribe unsubscribes the consumer.
Unsubscribe(ctx context.Context, consumer pubsub.Consumer) error
}
type event struct {
pubsub pubsub.PubSub
topic string
}
func New(pubsub pubsub.PubSub, topic string) EventsStreamer {
return &event{
pubsub: pubsub,
topic: topic,
}
}
func (e *event) Publish(ctx context.Context, spaceID int64, event *Event) error {
bytes, err := json.Marshal(event)
if err != nil {
return err
}
option := pubsub.WithPublishNamespace(format(spaceID))
return e.pubsub.Publish(ctx, e.topic, bytes, option)
}
// format creates the namespace name which will be spaces-<id>
func format(id int64) string {
return "spaces-" + strconv.Itoa(int(id))
}
func (e *event) Subscribe(ctx context.Context, spaceID int64) (<-chan *Event, <-chan error, pubsub.Consumer) {
chEvent := make(chan *Event, 100) // TODO: check best size here
chErr := make(chan error)
g := func(payload []byte) error {
event := &Event{}
err := json.Unmarshal(payload, event)
if err != nil {
// This should never happen
return err
}
select {
case chEvent <- event:
default:
}
return nil
}
option := pubsub.WithChannelNamespace(format(spaceID))
consumer := e.pubsub.Subscribe(ctx, e.topic, g, option)
return chEvent, chErr, consumer
}
func (e *event) Unsubscribe(ctx context.Context, consumer pubsub.Consumer) error {
return consumer.Unsubscribe(ctx, e.topic)
}

View File

@ -7,7 +7,7 @@ package file
import ( import (
"context" "context"
"fmt" "fmt"
"io/ioutil" "io"
"github.com/harness/gitness/gitrpc" "github.com/harness/gitness/gitrpc"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
@ -37,7 +37,7 @@ func (f *service) Get(
IncludeLatestCommit: false, IncludeLatestCommit: false,
}) })
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to read tree node: %w", err)
} }
// viewing Raw content is only supported for blob content // viewing Raw content is only supported for blob content
if treeNodeOutput.Node.Type != gitrpc.TreeNodeTypeBlob { if treeNodeOutput.Node.Type != gitrpc.TreeNodeTypeBlob {
@ -53,7 +53,7 @@ func (f *service) Get(
return nil, fmt.Errorf("failed to read blob from gitrpc: %w", err) return nil, fmt.Errorf("failed to read blob from gitrpc: %w", err)
} }
buf, err := ioutil.ReadAll(blobReader.Content) buf, err := io.ReadAll(blobReader.Content)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not read blob content from file: %w", err) return nil, fmt.Errorf("could not read blob content from file: %w", err)
} }

View File

@ -8,8 +8,6 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt"
"github.com/harness/gitness/livelog" "github.com/harness/gitness/livelog"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
@ -102,7 +100,8 @@ func (e *embedded) Detail(ctx context.Context, stage *drone.Stage) (*client.Cont
func (e *embedded) Update(ctx context.Context, stage *drone.Stage) error { func (e *embedded) Update(ctx context.Context, stage *drone.Stage) error {
var err error var err error
convertedStage := convertFromDroneStage(stage) convertedStage := convertFromDroneStage(stage)
if stage.Status == enum.CIStatusPending || stage.Status == enum.CIStatusRunning { status := enum.ParseCIStatus(stage.Status)
if status == enum.CIStatusPending || status == enum.CIStatusRunning {
err = e.manager.BeforeStage(ctx, convertedStage) err = e.manager.BeforeStage(ctx, convertedStage)
} else { } else {
err = e.manager.AfterStage(ctx, convertedStage) err = e.manager.AfterStage(ctx, convertedStage)
@ -115,7 +114,8 @@ func (e *embedded) Update(ctx context.Context, stage *drone.Stage) error {
func (e *embedded) UpdateStep(ctx context.Context, step *drone.Step) error { func (e *embedded) UpdateStep(ctx context.Context, step *drone.Step) error {
var err error var err error
convertedStep := convertFromDroneStep(step) convertedStep := convertFromDroneStep(step)
if step.Status == enum.CIStatusPending || step.Status == enum.CIStatusRunning { status := enum.ParseCIStatus(step.Status)
if status == enum.CIStatusPending || status == enum.CIStatusRunning {
err = e.manager.BeforeStep(ctx, convertedStep) err = e.manager.BeforeStep(ctx, convertedStep)
} else { } else {
err = e.manager.AfterStep(ctx, convertedStep) err = e.manager.AfterStep(ctx, convertedStep)
@ -125,15 +125,13 @@ func (e *embedded) UpdateStep(ctx context.Context, step *drone.Step) error {
} }
// Watch watches for build cancellation requests. // Watch watches for build cancellation requests.
func (e *embedded) Watch(ctx context.Context, stage int64) (bool, error) { func (e *embedded) Watch(ctx context.Context, executionID int64) (bool, error) {
// Implement Watch logic here return e.manager.Watch(ctx, executionID)
return false, errors.New("Not implemented")
} }
// Batch batch writes logs to the streaming logs. // Batch batch writes logs to the streaming logs.
func (e *embedded) Batch(ctx context.Context, step int64, lines []*drone.Line) error { func (e *embedded) Batch(ctx context.Context, step int64, lines []*drone.Line) error {
for _, l := range lines { for _, l := range lines {
fmt.Println("line is: ", l)
line := convertFromDroneLine(l) line := convertFromDroneLine(l)
err := e.manager.Write(ctx, step, line) err := e.manager.Write(ctx, step, line)
if err != nil { if err != nil {

View File

@ -10,6 +10,7 @@ import (
"github.com/harness/gitness/internal/pipeline/file" "github.com/harness/gitness/internal/pipeline/file"
"github.com/harness/gitness/livelog" "github.com/harness/gitness/livelog"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
"github.com/drone/drone-go/drone" "github.com/drone/drone-go/drone"
"github.com/drone/runner-go/client" "github.com/drone/runner-go/client"
@ -23,7 +24,7 @@ func convertToDroneStage(stage *types.Stage) *drone.Stage {
Name: stage.Name, Name: stage.Name,
Kind: stage.Kind, Kind: stage.Kind,
Type: stage.Type, Type: stage.Type,
Status: stage.Status, Status: string(stage.Status),
Error: stage.Error, Error: stage.Error,
ErrIgnore: stage.ErrIgnore, ErrIgnore: stage.ErrIgnore,
ExitCode: stage.ExitCode, ExitCode: stage.ExitCode,
@ -34,10 +35,10 @@ func convertToDroneStage(stage *types.Stage) *drone.Stage {
Kernel: stage.Kernel, Kernel: stage.Kernel,
Limit: stage.Limit, Limit: stage.Limit,
LimitRepo: stage.LimitRepo, LimitRepo: stage.LimitRepo,
Started: stage.Started, Started: stage.Started / 1e3, // Drone uses Unix() timestamps whereas we use UnixMilli()
Stopped: stage.Stopped, Stopped: stage.Stopped / 1e3, // Drone uses Unix() timestamps whereas we use UnixMilli()
Created: stage.Created, Created: stage.Created / 1e3, // Drone uses Unix() timestamps whereas we use UnixMilli()
Updated: stage.Updated, Updated: stage.Updated / 1e3, // Drone uses Unix() timestamps whereas we use UnixMilli()
Version: stage.Version, Version: stage.Version,
OnSuccess: stage.OnSuccess, OnSuccess: stage.OnSuccess,
OnFailure: stage.OnFailure, OnFailure: stage.OnFailure,
@ -61,12 +62,12 @@ func convertToDroneStep(step *types.Step) *drone.Step {
StageID: step.StageID, StageID: step.StageID,
Number: int(step.Number), Number: int(step.Number),
Name: step.Name, Name: step.Name,
Status: step.Status, Status: string(step.Status),
Error: step.Error, Error: step.Error,
ErrIgnore: step.ErrIgnore, ErrIgnore: step.ErrIgnore,
ExitCode: step.ExitCode, ExitCode: step.ExitCode,
Started: step.Started, Started: step.Started / 1e3, // Drone uses Unix() timestamps whereas we use UnixMilli()
Stopped: step.Stopped, Stopped: step.Stopped / 1e3, // Drone uses Unix() timestamps whereas we use UnixMilli()
Version: step.Version, Version: step.Version,
DependsOn: step.DependsOn, DependsOn: step.DependsOn,
Image: step.Image, Image: step.Image,
@ -81,12 +82,12 @@ func convertFromDroneStep(step *drone.Step) *types.Step {
StageID: step.StageID, StageID: step.StageID,
Number: int64(step.Number), Number: int64(step.Number),
Name: step.Name, Name: step.Name,
Status: step.Status, Status: enum.ParseCIStatus(step.Status),
Error: step.Error, Error: step.Error,
ErrIgnore: step.ErrIgnore, ErrIgnore: step.ErrIgnore,
ExitCode: step.ExitCode, ExitCode: step.ExitCode,
Started: step.Started, Started: step.Started * 1e3, // Drone uses Unix() timestamps whereas we use UnixMilli()
Stopped: step.Stopped, Stopped: step.Stopped * 1e3,
Version: step.Version, Version: step.Version,
DependsOn: step.DependsOn, DependsOn: step.DependsOn,
Image: step.Image, Image: step.Image,
@ -103,12 +104,12 @@ func convertFromDroneSteps(steps []*drone.Step) []*types.Step {
StageID: step.StageID, StageID: step.StageID,
Number: int64(step.Number), Number: int64(step.Number),
Name: step.Name, Name: step.Name,
Status: step.Status, Status: enum.ParseCIStatus(step.Status),
Error: step.Error, Error: step.Error,
ErrIgnore: step.ErrIgnore, ErrIgnore: step.ErrIgnore,
ExitCode: step.ExitCode, ExitCode: step.ExitCode,
Started: step.Started, Started: step.Started * 1e3, // Drone uses Unix() timestamps whereas we use UnixMilli()
Stopped: step.Stopped, Stopped: step.Stopped * 1e3, // Drone uses Unix() timestamps whereas we use UnixMilli()
Version: step.Version, Version: step.Version,
DependsOn: step.DependsOn, DependsOn: step.DependsOn,
Image: step.Image, Image: step.Image,
@ -127,7 +128,7 @@ func convertFromDroneStage(stage *drone.Stage) *types.Stage {
Name: stage.Name, Name: stage.Name,
Kind: stage.Kind, Kind: stage.Kind,
Type: stage.Type, Type: stage.Type,
Status: stage.Status, Status: enum.ParseCIStatus(stage.Status),
Error: stage.Error, Error: stage.Error,
ErrIgnore: stage.ErrIgnore, ErrIgnore: stage.ErrIgnore,
ExitCode: stage.ExitCode, ExitCode: stage.ExitCode,
@ -138,8 +139,8 @@ func convertFromDroneStage(stage *drone.Stage) *types.Stage {
Kernel: stage.Kernel, Kernel: stage.Kernel,
Limit: stage.Limit, Limit: stage.Limit,
LimitRepo: stage.LimitRepo, LimitRepo: stage.LimitRepo,
Started: stage.Started, Started: stage.Started * 1e3, // Drone uses Unix() timestamps whereas we use UnixMilli()
Stopped: stage.Stopped, Stopped: stage.Stopped * 1e3, // Drone uses Unix() timestamps whereas we use UnixMilli()
Version: stage.Version, Version: stage.Version,
OnSuccess: stage.OnSuccess, OnSuccess: stage.OnSuccess,
OnFailure: stage.OnFailure, OnFailure: stage.OnFailure,
@ -164,7 +165,7 @@ func convertToDroneBuild(execution *types.Execution) *drone.Build {
Trigger: execution.Trigger, Trigger: execution.Trigger,
Number: execution.Number, Number: execution.Number,
Parent: execution.Parent, Parent: execution.Parent,
Status: execution.Status, Status: string(execution.Status),
Error: execution.Error, Error: execution.Error,
Event: execution.Event, Event: execution.Event,
Action: execution.Action, Action: execution.Action,
@ -188,10 +189,10 @@ func convertToDroneBuild(execution *types.Execution) *drone.Build {
Deploy: execution.Deploy, Deploy: execution.Deploy,
DeployID: execution.DeployID, DeployID: execution.DeployID,
Debug: execution.Debug, Debug: execution.Debug,
Started: execution.Started, Started: execution.Started / 1e3, // Drone uses Unix() timestamps whereas we use UnixMilli()
Finished: execution.Finished, Finished: execution.Finished / 1e3, // Drone uses Unix() timestamps whereas we use UnixMilli()
Created: execution.Created, Created: execution.Created / 1e3, // Drone uses Unix() timestamps whereas we use UnixMilli()
Updated: execution.Updated, Updated: execution.Updated / 1e3, // Drone uses Unix() timestamps whereas we use UnixMilli()
Version: execution.Version, Version: execution.Version,
} }
} }

View File

@ -10,9 +10,9 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/harness/gitness/internal/pipeline/events"
"github.com/harness/gitness/internal/pipeline/file" "github.com/harness/gitness/internal/pipeline/file"
"github.com/harness/gitness/internal/pipeline/scheduler" "github.com/harness/gitness/internal/pipeline/scheduler"
"github.com/harness/gitness/internal/sse"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
urlprovider "github.com/harness/gitness/internal/url" urlprovider "github.com/harness/gitness/internal/url"
"github.com/harness/gitness/livelog" "github.com/harness/gitness/livelog"
@ -63,6 +63,9 @@ type (
// Request requests the next available build stage for execution. // Request requests the next available build stage for execution.
Request(ctx context.Context, args *Request) (*types.Stage, error) Request(ctx context.Context, args *Request) (*types.Stage, error)
// Watch watches for build cancellation requests.
Watch(ctx context.Context, executionID int64) (bool, error)
// Accept accepts the build stage for execution. // Accept accepts the build stage for execution.
Accept(ctx context.Context, stage int64, machine string) (*types.Stage, error) Accept(ctx context.Context, stage int64, machine string) (*types.Stage, error)
@ -97,8 +100,9 @@ type Manager struct {
FileService file.FileService FileService file.FileService
Pipelines store.PipelineStore Pipelines store.PipelineStore
urlProvider *urlprovider.Provider urlProvider *urlprovider.Provider
Checks store.CheckStore
// Converter store.ConvertService // Converter store.ConvertService
Events events.EventsStreamer SSEStreamer sse.Streamer
// Globals store.GlobalSecretStore // Globals store.GlobalSecretStore
Logs store.LogStore Logs store.LogStore
Logz livelog.LogStream Logz livelog.LogStream
@ -119,10 +123,11 @@ func New(
executionStore store.ExecutionStore, executionStore store.ExecutionStore,
pipelineStore store.PipelineStore, pipelineStore store.PipelineStore,
urlProvider *urlprovider.Provider, urlProvider *urlprovider.Provider,
events events.EventsStreamer, sseStreamer sse.Streamer,
fileService file.FileService, fileService file.FileService,
logStore store.LogStore, logStore store.LogStore,
logStream livelog.LogStream, logStream livelog.LogStream,
checkStore store.CheckStore,
repoStore store.RepoStore, repoStore store.RepoStore,
scheduler scheduler.Scheduler, scheduler scheduler.Scheduler,
secretStore store.SecretStore, secretStore store.SecretStore,
@ -135,10 +140,11 @@ func New(
Executions: executionStore, Executions: executionStore,
Pipelines: pipelineStore, Pipelines: pipelineStore,
urlProvider: urlProvider, urlProvider: urlProvider,
Events: events, SSEStreamer: sseStreamer,
FileService: fileService, FileService: fileService,
Logs: logStore, Logs: logStore,
Logz: logStream, Logz: logStream,
Checks: checkStore,
Repos: repoStore, Repos: repoStore,
Scheduler: scheduler, Scheduler: scheduler,
Secrets: secretStore, Secrets: secretStore,
@ -300,7 +306,7 @@ func (m *Manager) Details(ctx context.Context, stageID int64) (*ExecutionContext
// Before signals the build step is about to start. // Before signals the build step is about to start.
func (m *Manager) BeforeStep(ctx context.Context, step *types.Step) error { func (m *Manager) BeforeStep(ctx context.Context, step *types.Step) error {
log := log.With(). log := log.With().
Str("step.status", step.Status). Str("step.status", string(step.Status)).
Str("step.name", step.Name). Str("step.name", step.Name).
Int64("step.id", step.ID). Int64("step.id", step.ID).
Logger() Logger()
@ -314,7 +320,7 @@ func (m *Manager) BeforeStep(ctx context.Context, step *types.Step) error {
} }
updater := &updater{ updater := &updater{
Executions: m.Executions, Executions: m.Executions,
Events: m.Events, SSEStreamer: m.SSEStreamer,
Repos: m.Repos, Repos: m.Repos,
Steps: m.Steps, Steps: m.Steps,
Stages: m.Stages, Stages: m.Stages,
@ -325,7 +331,7 @@ func (m *Manager) BeforeStep(ctx context.Context, step *types.Step) error {
// After signals the build step is complete. // After signals the build step is complete.
func (m *Manager) AfterStep(ctx context.Context, step *types.Step) error { func (m *Manager) AfterStep(ctx context.Context, step *types.Step) error {
log := log.With(). log := log.With().
Str("step.status", step.Status). Str("step.status", string(step.Status)).
Str("step.name", step.Name). Str("step.name", step.Name).
Int64("step.id", step.ID). Int64("step.id", step.ID).
Logger() Logger()
@ -334,7 +340,7 @@ func (m *Manager) AfterStep(ctx context.Context, step *types.Step) error {
var retErr error var retErr error
updater := &updater{ updater := &updater{
Executions: m.Executions, Executions: m.Executions,
Events: m.Events, SSEStreamer: m.SSEStreamer,
Repos: m.Repos, Repos: m.Repos,
Steps: m.Steps, Steps: m.Steps,
Stages: m.Stages, Stages: m.Stages,
@ -355,7 +361,9 @@ func (m *Manager) AfterStep(ctx context.Context, step *types.Step) error {
func (m *Manager) BeforeStage(ctx context.Context, stage *types.Stage) error { func (m *Manager) BeforeStage(ctx context.Context, stage *types.Stage) error {
s := &setup{ s := &setup{
Executions: m.Executions, Executions: m.Executions,
Events: m.Events, Checks: m.Checks,
Pipelines: m.Pipelines,
SSEStreamer: m.SSEStreamer,
Repos: m.Repos, Repos: m.Repos,
Steps: m.Steps, Steps: m.Steps,
Stages: m.Stages, Stages: m.Stages,
@ -369,7 +377,9 @@ func (m *Manager) BeforeStage(ctx context.Context, stage *types.Stage) error {
func (m *Manager) AfterStage(ctx context.Context, stage *types.Stage) error { func (m *Manager) AfterStage(ctx context.Context, stage *types.Stage) error {
t := &teardown{ t := &teardown{
Executions: m.Executions, Executions: m.Executions,
Events: m.Events, Pipelines: m.Pipelines,
Checks: m.Checks,
SSEStreamer: m.SSEStreamer,
Logs: m.Logz, Logs: m.Logz,
Repos: m.Repos, Repos: m.Repos,
Scheduler: m.Scheduler, Scheduler: m.Scheduler,
@ -378,3 +388,36 @@ func (m *Manager) AfterStage(ctx context.Context, stage *types.Stage) error {
} }
return t.do(noContext, stage) return t.do(noContext, stage)
} }
// Watch watches for build cancellation requests.
func (m *Manager) Watch(ctx context.Context, executionID int64) (bool, error) {
ok, err := m.Scheduler.Cancelled(ctx, executionID)
// we expect a context cancel error here which
// indicates a polling timeout. The subscribing
// client should look for the context cancel error
// and resume polling.
if err != nil {
return ok, err
}
// // TODO: we should be able to return
// // immediately if Cancelled returns true. This requires
// // some more testing but would avoid the extra database
// // call.
// if ok {
// return ok, err
// }
// if no error is returned we should check
// the database to see if the build is complete. If
// complete, return true.
execution, err := m.Executions.Find(ctx, executionID)
if err != nil {
log := log.With().
Int64("execution.id", executionID).
Logger()
log.Warn().Msg("manager: cannot find build")
return ok, fmt.Errorf("could not find build for cancellation: %w", err)
}
return execution.Status.IsDone(), nil
}

View File

@ -6,11 +6,11 @@ package manager
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"time" "time"
"github.com/harness/gitness/internal/pipeline/events" "github.com/harness/gitness/internal/pipeline/checks"
"github.com/harness/gitness/internal/sse"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
gitness_store "github.com/harness/gitness/store" gitness_store "github.com/harness/gitness/store"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
@ -21,7 +21,9 @@ import (
type setup struct { type setup struct {
Executions store.ExecutionStore Executions store.ExecutionStore
Events events.EventsStreamer Checks store.CheckStore
SSEStreamer sse.Streamer
Pipelines store.PipelineStore
Repos store.RepoStore Repos store.RepoStore
Steps store.StepStore Steps store.StepStore
Stages store.StageStore Stages store.StageStore
@ -54,7 +56,7 @@ func (s *setup) do(ctx context.Context, stage *types.Stage) error {
err = s.Stages.Update(noContext, stage) err = s.Stages.Update(noContext, stage)
if err != nil { if err != nil {
log.Error().Err(err). log.Error().Err(err).
Str("stage.status", stage.Status). Str("stage.status", string(stage.Status)).
Msg("manager: cannot update the stage") Msg("manager: cannot update the stage")
return err return err
} }
@ -67,7 +69,7 @@ func (s *setup) do(ctx context.Context, stage *types.Stage) error {
err := s.Steps.Create(noContext, step) err := s.Steps.Create(noContext, step)
if err != nil { if err != nil {
log.Error().Err(err). log.Error().Err(err).
Str("stage.status", stage.Status). Str("stage.status", string(stage.Status)).
Str("step.name", step.Name). Str("step.name", step.Name).
Int64("step.id", step.ID). Int64("step.id", step.ID).
Msg("manager: cannot persist the step") Msg("manager: cannot persist the step")
@ -80,13 +82,23 @@ func (s *setup) do(ctx context.Context, stage *types.Stage) error {
log.Error().Err(err).Msg("manager: cannot update the execution") log.Error().Err(err).Msg("manager: cannot update the execution")
return err return err
} }
pipeline, err := s.Pipelines.Find(ctx, execution.PipelineID)
if err != nil {
log.Error().Err(err).Msg("manager: cannot find pipeline")
return err
}
// try to write to the checks store - if not, log an error and continue
err = checks.Write(ctx, s.Checks, execution, pipeline)
if err != nil {
log.Error().Err(err).Msg("manager: could not write to checks store")
}
stages, err := s.Stages.ListWithSteps(noContext, execution.ID) stages, err := s.Stages.ListWithSteps(noContext, execution.ID)
if err != nil { if err != nil {
log.Error().Err(err).Msg("manager: could not list stages with steps") log.Error().Err(err).Msg("manager: could not list stages with steps")
return err return err
} }
execution.Stages = stages execution.Stages = stages
err = s.Events.Publish(noContext, repo.ParentID, executionEvent(enum.ExecutionRunning, execution)) err = s.SSEStreamer.Publish(noContext, repo.ParentID, enum.SSETypeExecutionRunning, execution)
if err != nil { if err != nil {
log.Warn().Err(err).Msg("manager: could not publish execution event") log.Warn().Err(err).Msg("manager: could not publish execution event")
} }
@ -94,18 +106,6 @@ func (s *setup) do(ctx context.Context, stage *types.Stage) error {
return nil return nil
} }
func executionEvent(
eventType enum.EventType,
execution *types.Execution,
) *events.Event {
// json.Marshal will not return an error here, it can be absorbed
bytes, _ := json.Marshal(execution)
return &events.Event{
Type: eventType,
Data: bytes,
}
}
// helper function that updates the execution status from pending to running. // helper function that updates the execution status from pending to running.
// This accounts for the fact that another agent may have already updated // This accounts for the fact that another agent may have already updated
// the execution status, which may happen if two stages execute concurrently. // the execution status, which may happen if two stages execute concurrently.

View File

@ -8,8 +8,9 @@ import (
"context" "context"
"time" "time"
"github.com/harness/gitness/internal/pipeline/events" "github.com/harness/gitness/internal/pipeline/checks"
"github.com/harness/gitness/internal/pipeline/scheduler" "github.com/harness/gitness/internal/pipeline/scheduler"
"github.com/harness/gitness/internal/sse"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/livelog" "github.com/harness/gitness/livelog"
gitness_store "github.com/harness/gitness/store" gitness_store "github.com/harness/gitness/store"
@ -21,7 +22,9 @@ import (
type teardown struct { type teardown struct {
Executions store.ExecutionStore Executions store.ExecutionStore
Events events.EventsStreamer Checks store.CheckStore
Pipelines store.PipelineStore
SSEStreamer sse.Streamer
Logs livelog.LogStream Logs livelog.LogStream
Scheduler scheduler.Scheduler Scheduler scheduler.Scheduler
Repos store.RepoStore Repos store.RepoStore
@ -45,7 +48,7 @@ func (t *teardown) do(ctx context.Context, stage *types.Stage) error {
Int64("execution.number", execution.Number). Int64("execution.number", execution.Number).
Int64("execution.id", execution.ID). Int64("execution.id", execution.ID).
Int64("repo.id", execution.RepoID). Int64("repo.id", execution.RepoID).
Str("stage.status", stage.Status). Str("stage.status", string(stage.Status)).
Logger() Logger()
repo, err := t.Repos.Find(noContext, execution.RepoID) repo, err := t.Repos.Find(noContext, execution.RepoID)
@ -134,12 +137,23 @@ func (t *teardown) do(ctx context.Context, stage *types.Stage) error {
} }
execution.Stages = stages execution.Stages = stages
err = t.Events.Publish(noContext, repo.ParentID, executionEvent(enum.ExecutionCompleted, execution)) err = t.SSEStreamer.Publish(noContext, repo.ParentID, enum.SSETypeExecutionCompleted, execution)
if err != nil { if err != nil {
log.Warn().Err(err). log.Warn().Err(err).
Msg("manager: could not publish execution completed event") Msg("manager: could not publish execution completed event")
} }
pipeline, err := t.Pipelines.Find(ctx, execution.PipelineID)
if err != nil {
log.Error().Err(err).Msg("manager: cannot find pipeline")
return err
}
// try to write to the checks store - if not, log an error and continue
err = checks.Write(ctx, t.Checks, execution, pipeline)
if err != nil {
log.Error().Err(err).Msg("manager: could not write to checks store")
}
return nil return nil
} }

View File

@ -7,7 +7,7 @@ package manager
import ( import (
"context" "context"
"github.com/harness/gitness/internal/pipeline/events" "github.com/harness/gitness/internal/sse"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
@ -18,7 +18,7 @@ import (
type updater struct { type updater struct {
Executions store.ExecutionStore Executions store.ExecutionStore
Repos store.RepoStore Repos store.RepoStore
Events events.EventsStreamer SSEStreamer sse.Streamer
Steps store.StepStore Steps store.StepStore
Stages store.StageStore Stages store.StageStore
} }
@ -26,7 +26,7 @@ type updater struct {
func (u *updater) do(ctx context.Context, step *types.Step) error { func (u *updater) do(ctx context.Context, step *types.Step) error {
log := log.With(). log := log.With().
Str("step.name", step.Name). Str("step.name", step.Name).
Str("step.status", step.Status). Str("step.status", string(step.Status)).
Int64("step.id", step.ID). Int64("step.id", step.ID).
Logger() Logger()
@ -64,7 +64,7 @@ func (u *updater) do(ctx context.Context, step *types.Step) error {
} }
execution.Stages = stages execution.Stages = stages
err = u.Events.Publish(noContext, repo.ParentID, executionEvent(enum.ExecutionUpdated, execution)) err = u.SSEStreamer.Publish(noContext, repo.ParentID, enum.SSETypeExecutionUpdated, execution)
if err != nil { if err != nil {
log.Warn().Err(err).Msg("manager: cannot publish execution updated event") log.Warn().Err(err).Msg("manager: cannot publish execution updated event")
} }

View File

@ -5,9 +5,9 @@
package manager package manager
import ( import (
"github.com/harness/gitness/internal/pipeline/events"
"github.com/harness/gitness/internal/pipeline/file" "github.com/harness/gitness/internal/pipeline/file"
"github.com/harness/gitness/internal/pipeline/scheduler" "github.com/harness/gitness/internal/pipeline/scheduler"
"github.com/harness/gitness/internal/sse"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/internal/url" "github.com/harness/gitness/internal/url"
"github.com/harness/gitness/livelog" "github.com/harness/gitness/livelog"
@ -29,18 +29,19 @@ func ProvideExecutionManager(
executionStore store.ExecutionStore, executionStore store.ExecutionStore,
pipelineStore store.PipelineStore, pipelineStore store.PipelineStore,
urlProvider *url.Provider, urlProvider *url.Provider,
events events.EventsStreamer, sseStreamer sse.Streamer,
fileService file.FileService, fileService file.FileService,
logStore store.LogStore, logStore store.LogStore,
logStream livelog.LogStream, logStream livelog.LogStream,
checkStore store.CheckStore,
repoStore store.RepoStore, repoStore store.RepoStore,
scheduler scheduler.Scheduler, scheduler scheduler.Scheduler,
secretStore store.SecretStore, secretStore store.SecretStore,
stageStore store.StageStore, stageStore store.StageStore,
stepStore store.StepStore, stepStore store.StepStore,
userStore store.PrincipalStore) ExecutionManager { userStore store.PrincipalStore) ExecutionManager {
return New(config, executionStore, pipelineStore, urlProvider, events, fileService, logStore, return New(config, executionStore, pipelineStore, urlProvider, sseStreamer, fileService, logStore,
logStream, repoStore, scheduler, secretStore, stageStore, stepStore, userStore) logStream, checkStore, repoStore, scheduler, secretStore, stageStore, stepStore, userStore)
} }
// ProvideExecutionClient provides a client implementation to interact with the execution manager. // ProvideExecutionClient provides a client implementation to interact with the execution manager.

View File

@ -0,0 +1,80 @@
// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
package scheduler
import (
"context"
"sync"
"time"
)
type canceler struct {
sync.Mutex
subscribers map[chan struct{}]int64
cancelled map[int64]time.Time
}
func newCanceler() *canceler {
return &canceler{
subscribers: make(map[chan struct{}]int64),
cancelled: make(map[int64]time.Time),
}
}
func (c *canceler) Cancel(ctx context.Context, id int64) error {
c.Lock()
defer c.Unlock()
c.cancelled[id] = time.Now().Add(time.Minute * 5)
for subscriber, build := range c.subscribers {
if id == build {
close(subscriber)
}
}
c.collect()
return nil
}
func (c *canceler) Cancelled(ctx context.Context, id int64) (bool, error) {
subscriber := make(chan struct{})
c.Lock()
c.subscribers[subscriber] = id
c.Unlock()
defer func() {
c.Lock()
delete(c.subscribers, subscriber)
c.Unlock()
}()
for {
select {
case <-ctx.Done():
return false, ctx.Err()
case <-time.After(time.Minute):
c.Lock()
_, ok := c.cancelled[id]
c.Unlock()
if ok {
return true, nil
}
case <-subscriber:
return true, nil
}
}
}
func (c *canceler) collect() {
// the list of cancelled builds is stored with a ttl, and
// is not removed until the ttl is reached. This provides
// adequate window for clients with connectivity issues to
// reconnect and receive notification of cancel events.
now := time.Now()
for build, timestamp := range c.cancelled {
if now.After(timestamp) {
delete(c.cancelled, build)
}
}
}

View File

@ -7,6 +7,8 @@ package scheduler
import ( import (
"context" "context"
"github.com/harness/gitness/internal/store"
"github.com/harness/gitness/lock"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
) )
@ -29,4 +31,29 @@ type Scheduler interface {
// Request requests the next stage scheduled for execution. // Request requests the next stage scheduled for execution.
Request(ctx context.Context, filter Filter) (*types.Stage, error) Request(ctx context.Context, filter Filter) (*types.Stage, error)
// Cancel cancels scheduled or running jobs associated
// with the parent build ID.
Cancel(context.Context, int64) error
// Cancelled blocks and listens for a cancellation event and
// returns true if the build has been cancelled.
Cancelled(context.Context, int64) (bool, error)
}
type scheduler struct {
*queue
*canceler
}
// newScheduler provides an instance of a scheduler with cancel abilities
func newScheduler(stageStore store.StageStore, lock lock.MutexManager) (Scheduler, error) {
q, err := newQueue(stageStore, lock)
if err != nil {
return nil, err
}
return scheduler{
q,
newCanceler(),
}, nil
} }

View File

@ -21,5 +21,5 @@ func ProvideScheduler(
stageStore store.StageStore, stageStore store.StageStore,
lock lock.MutexManager, lock lock.MutexManager,
) (Scheduler, error) { ) (Scheduler, error) {
return newQueue(stageStore, lock) return newScheduler(stageStore, lock)
} }

View File

@ -9,6 +9,7 @@ import (
"runtime/debug" "runtime/debug"
"time" "time"
"github.com/harness/gitness/internal/pipeline/checks"
"github.com/harness/gitness/internal/pipeline/file" "github.com/harness/gitness/internal/pipeline/file"
"github.com/harness/gitness/internal/pipeline/scheduler" "github.com/harness/gitness/internal/pipeline/scheduler"
"github.com/harness/gitness/internal/pipeline/triggerer/dag" "github.com/harness/gitness/internal/pipeline/triggerer/dag"
@ -29,6 +30,7 @@ var _ Triggerer = (*triggerer)(nil)
type Hook struct { type Hook struct {
Parent int64 `json:"parent"` Parent int64 `json:"parent"`
Trigger string `json:"trigger"` Trigger string `json:"trigger"`
TriggeredBy int64 `json:"triggered_by"`
Action enum.TriggerAction `json:"action"` Action enum.TriggerAction `json:"action"`
Link string `json:"link"` Link string `json:"link"`
Timestamp int64 `json:"timestamp"` Timestamp int64 `json:"timestamp"`
@ -59,6 +61,7 @@ type Triggerer interface {
type triggerer struct { type triggerer struct {
executionStore store.ExecutionStore executionStore store.ExecutionStore
checkStore store.CheckStore
stageStore store.StageStore stageStore store.StageStore
db *sqlx.DB db *sqlx.DB
pipelineStore store.PipelineStore pipelineStore store.PipelineStore
@ -69,6 +72,7 @@ type triggerer struct {
func New( func New(
executionStore store.ExecutionStore, executionStore store.ExecutionStore,
checkStore store.CheckStore,
stageStore store.StageStore, stageStore store.StageStore,
pipelineStore store.PipelineStore, pipelineStore store.PipelineStore,
db *sqlx.DB, db *sqlx.DB,
@ -78,6 +82,7 @@ func New(
) *triggerer { ) *triggerer {
return &triggerer{ return &triggerer{
executionStore: executionStore, executionStore: executionStore,
checkStore: checkStore,
stageStore: stageStore, stageStore: stageStore,
scheduler: scheduler, scheduler: scheduler,
db: db, db: db,
@ -279,6 +284,7 @@ func (t *triggerer) Trigger(
RepoID: repo.ID, RepoID: repo.ID,
PipelineID: pipeline.ID, PipelineID: pipeline.ID,
Trigger: base.Trigger, Trigger: base.Trigger,
CreatedBy: base.TriggeredBy,
Number: pipeline.Seq, Number: pipeline.Seq,
Parent: base.Parent, Parent: base.Parent,
Status: enum.CIStatusPending, Status: enum.CIStatusPending,
@ -308,8 +314,8 @@ func (t *triggerer) Trigger(
stages := make([]*types.Stage, len(matched)) stages := make([]*types.Stage, len(matched))
for i, match := range matched { for i, match := range matched {
onSuccess := match.Trigger.Status.Match(enum.CIStatusSuccess) onSuccess := match.Trigger.Status.Match(string(enum.CIStatusSuccess))
onFailure := match.Trigger.Status.Match(enum.CIStatusFailure) onFailure := match.Trigger.Status.Match(string(enum.CIStatusFailure))
if len(match.Trigger.Status.Include)+len(match.Trigger.Status.Exclude) == 0 { if len(match.Trigger.Status.Include)+len(match.Trigger.Status.Exclude) == 0 {
onFailure = false onFailure = false
} }
@ -377,6 +383,12 @@ func (t *triggerer) Trigger(
return nil, err return nil, err
} }
// try to write to check store. log on failure but don't error out the execution
err = checks.Write(ctx, t.checkStore, execution, pipeline)
if err != nil {
log.Error().Err(err).Msg("trigger: could not write to check store")
}
// err = t.status.Send(ctx, user, &core.StatusInput{ // err = t.status.Send(ctx, user, &core.StatusInput{
// Repo: repo, // Repo: repo,
// Execution: execution, // Execution: execution,
@ -453,6 +465,7 @@ func (t *triggerer) createExecutionWithError(
execution := &types.Execution{ execution := &types.Execution{
RepoID: pipeline.RepoID, RepoID: pipeline.RepoID,
PipelineID: pipeline.ID,
Number: pipeline.Seq, Number: pipeline.Seq,
Parent: base.Parent, Parent: base.Parent,
Status: enum.CIStatusError, Status: enum.CIStatusError,
@ -462,6 +475,7 @@ func (t *triggerer) createExecutionWithError(
Link: base.Link, Link: base.Link,
Title: base.Title, Title: base.Title,
Message: base.Message, Message: base.Message,
CreatedBy: base.TriggeredBy,
Before: base.Before, Before: base.Before,
After: base.After, After: base.After,
Ref: base.Ref, Ref: base.Ref,
@ -486,5 +500,11 @@ func (t *triggerer) createExecutionWithError(
return nil, err return nil, err
} }
// try to write to check store, log on failure
err = checks.Write(ctx, t.checkStore, execution, pipeline)
if err != nil {
log.Error().Err(err).Msg("trigger: failed to update check")
}
return execution, nil return execution, nil
} }

View File

@ -21,6 +21,7 @@ var WireSet = wire.NewSet(
// ProvideTriggerer provides a triggerer which can execute builds. // ProvideTriggerer provides a triggerer which can execute builds.
func ProvideTriggerer( func ProvideTriggerer(
executionStore store.ExecutionStore, executionStore store.ExecutionStore,
checkStore store.CheckStore,
stageStore store.StageStore, stageStore store.StageStore,
db *sqlx.DB, db *sqlx.DB,
pipelineStore store.PipelineStore, pipelineStore store.PipelineStore,
@ -28,6 +29,6 @@ func ProvideTriggerer(
scheduler scheduler.Scheduler, scheduler scheduler.Scheduler,
repoStore store.RepoStore, repoStore store.RepoStore,
) Triggerer { ) Triggerer {
return New(executionStore, stageStore, pipelineStore, return New(executionStore, checkStore, stageStore, pipelineStore,
db, repoStore, scheduler, fileService) db, repoStore, scheduler, fileService)
} }

View File

@ -189,7 +189,7 @@ func setupSpaces(r chi.Router, spaceCtrl *space.Controller) {
r.Patch("/", handlerspace.HandleUpdate(spaceCtrl)) r.Patch("/", handlerspace.HandleUpdate(spaceCtrl))
r.Delete("/", handlerspace.HandleDelete(spaceCtrl)) r.Delete("/", handlerspace.HandleDelete(spaceCtrl))
r.Get("/stream", handlerspace.HandleEventsStream(spaceCtrl)) r.Get("/events", handlerspace.HandleEvents(spaceCtrl))
r.Post("/move", handlerspace.HandleMove(spaceCtrl)) r.Post("/move", handlerspace.HandleMove(spaceCtrl))
r.Get("/spaces", handlerspace.HandleListSpaces(spaceCtrl)) r.Get("/spaces", handlerspace.HandleListSpaces(spaceCtrl))
@ -408,7 +408,7 @@ func setupExecutions(
r.Post("/", handlerexecution.HandleCreate(executionCtrl)) r.Post("/", handlerexecution.HandleCreate(executionCtrl))
r.Route(fmt.Sprintf("/{%s}", request.PathParamExecutionNumber), func(r chi.Router) { r.Route(fmt.Sprintf("/{%s}", request.PathParamExecutionNumber), func(r chi.Router) {
r.Get("/", handlerexecution.HandleFind(executionCtrl)) r.Get("/", handlerexecution.HandleFind(executionCtrl))
r.Patch("/", handlerexecution.HandleUpdate(executionCtrl)) r.Post("/cancel", handlerexecution.HandleCancel(executionCtrl))
r.Delete("/", handlerexecution.HandleDelete(executionCtrl)) r.Delete("/", handlerexecution.HandleDelete(executionCtrl))
r.Get( r.Get(
fmt.Sprintf("/logs/{%s}/{%s}", fmt.Sprintf("/logs/{%s}/{%s}",

View File

@ -0,0 +1,24 @@
// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
package importer
import (
"strconv"
"strings"
)
const jobIDPrefix = "import-repo-"
func JobIDFromRepoID(repoID int64) string {
return jobIDPrefix + strconv.FormatInt(repoID, 10)
}
func RepoIDFromJobID(jobID string) int64 {
if !strings.HasPrefix(jobID, jobIDPrefix) {
return 0
}
repoID, _ := strconv.ParseInt(jobID[len(jobIDPrefix):], 10, 64)
return repoID
}

View File

@ -6,7 +6,8 @@ package importer
import ( import (
"context" "context"
"errors" "crypto/sha512"
"encoding/base32"
"fmt" "fmt"
"net/http" "net/http"
"time" "time"
@ -55,11 +56,10 @@ func (r *RepositoryInfo) ToRepo(
path string, path string,
uid string, uid string,
description string, description string,
jobUID string,
principal *types.Principal, principal *types.Principal,
) *types.Repository { ) *types.Repository {
now := time.Now().UnixMilli() now := time.Now().UnixMilli()
gitTempUID := "importing-" + jobUID gitTempUID := fmt.Sprintf("importing-%s-%d", hash(path), now)
return &types.Repository{ return &types.Repository{
Version: 0, Version: 0,
ParentID: spaceID, ParentID: spaceID,
@ -74,12 +74,17 @@ func (r *RepositoryInfo) ToRepo(
ForkID: 0, ForkID: 0,
DefaultBranch: r.DefaultBranch, DefaultBranch: r.DefaultBranch,
Importing: true, Importing: true,
ImportingJobUID: &jobUID,
} }
} }
func getClient(provider Provider) (*scm.Client, error) { func hash(s string) string {
if provider.Username == "" || provider.Password == "" { h := sha512.New()
_, _ = h.Write([]byte(s))
return base32.StdEncoding.EncodeToString(h.Sum(nil)[:10])
}
func getClient(provider Provider, authReq bool) (*scm.Client, error) {
if authReq && (provider.Username == "" || provider.Password == "") {
return nil, usererror.BadRequest("scm provider authentication credentials missing") return nil, usererror.BadRequest("scm provider authentication credentials missing")
} }
@ -114,16 +119,18 @@ func getClient(provider Provider) (*scm.Client, error) {
return nil, usererror.BadRequestf("unsupported scm provider: %s", provider) return nil, usererror.BadRequestf("unsupported scm provider: %s", provider)
} }
if provider.Password != "" {
c.Client = &http.Client{ c.Client = &http.Client{
Transport: &oauth2.Transport{ Transport: &oauth2.Transport{
Source: oauth2.StaticTokenSource(&scm.Token{Token: provider.Password}), Source: oauth2.StaticTokenSource(&scm.Token{Token: provider.Password}),
}, },
} }
}
return c, nil return c, nil
} }
func LoadRepositoryFromProvider(ctx context.Context, provider Provider, repoSlug string) (RepositoryInfo, error) { func LoadRepositoryFromProvider(ctx context.Context, provider Provider, repoSlug string) (RepositoryInfo, error) {
scmClient, err := getClient(provider) scmClient, err := getClient(provider, false)
if err != nil { if err != nil {
return RepositoryInfo{}, err return RepositoryInfo{}, err
} }
@ -132,24 +139,9 @@ func LoadRepositoryFromProvider(ctx context.Context, provider Provider, repoSlug
return RepositoryInfo{}, usererror.BadRequest("provider repository identifier is missing") return RepositoryInfo{}, usererror.BadRequest("provider repository identifier is missing")
} }
var statusCode int
scmRepo, scmResp, err := scmClient.Repositories.Find(ctx, repoSlug) scmRepo, scmResp, err := scmClient.Repositories.Find(ctx, repoSlug)
if scmResp != nil { if err = convertSCMError(provider, repoSlug, scmResp, err); err != nil {
statusCode = scmResp.Status return RepositoryInfo{}, err
}
if errors.Is(err, scm.ErrNotFound) || statusCode == http.StatusNotFound {
return RepositoryInfo{},
usererror.BadRequestf("repository %s not found at %s", repoSlug, provider.Type)
}
if errors.Is(err, scm.ErrNotAuthorized) || statusCode == http.StatusUnauthorized {
return RepositoryInfo{},
usererror.BadRequestf("bad credentials provided for %s at %s", repoSlug, provider.Type)
}
if err != nil || statusCode > 299 {
return RepositoryInfo{},
fmt.Errorf("failed to fetch repository %s from %s, status=%d: %w",
repoSlug, provider.Type, statusCode, err)
} }
return RepositoryInfo{ return RepositoryInfo{
@ -162,7 +154,7 @@ func LoadRepositoryFromProvider(ctx context.Context, provider Provider, repoSlug
} }
func LoadRepositoriesFromProviderSpace(ctx context.Context, provider Provider, spaceSlug string) ([]RepositoryInfo, error) { func LoadRepositoriesFromProviderSpace(ctx context.Context, provider Provider, spaceSlug string) ([]RepositoryInfo, error) {
scmClient, err := getClient(provider) scmClient, err := getClient(provider, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -172,27 +164,23 @@ func LoadRepositoriesFromProviderSpace(ctx context.Context, provider Provider, s
} }
const pageSize = 100 const pageSize = 100
opts := scm.ListOptions{Page: 0, Size: pageSize} opts := scm.RepoListOptions{
ListOptions: scm.ListOptions{
Page: 0,
Size: pageSize,
},
RepoSearchTerm: scm.RepoSearchTerm{
User: spaceSlug,
},
}
repos := make([]RepositoryInfo, 0) repos := make([]RepositoryInfo, 0)
for { for {
opts.Page++ opts.Page++
var statusCode int scmRepos, scmResp, err := scmClient.Repositories.ListV2(ctx, opts)
scmRepos, scmResp, err := scmClient.Repositories.List(ctx, opts) if err = convertSCMError(provider, spaceSlug, scmResp, err); err != nil {
if scmResp != nil { return nil, err
statusCode = scmResp.Status
}
if errors.Is(err, scm.ErrNotFound) || statusCode == http.StatusNotFound {
return nil, usererror.BadRequestf("space %s not found at %s", spaceSlug, provider.Type)
}
if errors.Is(err, scm.ErrNotAuthorized) || statusCode == http.StatusUnauthorized {
return nil, usererror.BadRequestf("bad credentials provided for %s at %s", spaceSlug, provider.Type)
}
if err != nil || statusCode > 299 {
return nil, fmt.Errorf("failed to fetch space %s from %s, status=%d: %w",
spaceSlug, provider.Type, statusCode, err)
} }
if len(scmRepos) == 0 { if len(scmRepos) == 0 {
@ -200,9 +188,6 @@ func LoadRepositoriesFromProviderSpace(ctx context.Context, provider Provider, s
} }
for _, scmRepo := range scmRepos { for _, scmRepo := range scmRepos {
if scmRepo.Namespace != spaceSlug {
continue
}
repos = append(repos, RepositoryInfo{ repos = append(repos, RepositoryInfo{
Space: scmRepo.Namespace, Space: scmRepo.Namespace,
UID: scmRepo.Name, UID: scmRepo.Name,
@ -215,3 +200,34 @@ func LoadRepositoriesFromProviderSpace(ctx context.Context, provider Provider, s
return repos, nil return repos, nil
} }
func convertSCMError(provider Provider, slug string, r *scm.Response, err error) error {
if err == nil {
return nil
}
if r == nil {
if provider.Host != "" {
return usererror.BadRequestf("failed to make HTTP request to %s (host=%s): %s",
provider.Type, provider.Host, err)
} else {
return usererror.BadRequestf("failed to make HTTP request to %s: %s",
provider.Type, err)
}
}
switch r.Status {
case http.StatusNotFound:
return usererror.BadRequestf("couldn't find %s at %s: %s",
slug, provider.Type, err.Error())
case http.StatusUnauthorized:
return usererror.BadRequestf("bad credentials provided for %s at %s: %s",
slug, provider.Type, err.Error())
case http.StatusForbidden:
return usererror.BadRequestf("access denied to %s at %s: %s",
slug, provider.Type, err.Error())
default:
return usererror.BadRequestf("failed to fetch %s from %s (HTTP status %d): %s",
slug, provider.Type, r.Status, err.Error())
}
}

View File

@ -19,10 +19,12 @@ import (
"github.com/harness/gitness/internal/bootstrap" "github.com/harness/gitness/internal/bootstrap"
"github.com/harness/gitness/internal/githook" "github.com/harness/gitness/internal/githook"
"github.com/harness/gitness/internal/services/job" "github.com/harness/gitness/internal/services/job"
"github.com/harness/gitness/internal/sse"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
gitnessurl "github.com/harness/gitness/internal/url" gitnessurl "github.com/harness/gitness/internal/url"
gitness_store "github.com/harness/gitness/store" gitness_store "github.com/harness/gitness/store"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -39,6 +41,7 @@ type Repository struct {
repoStore store.RepoStore repoStore store.RepoStore
encrypter encrypt.Encrypter encrypter encrypt.Encrypter
scheduler *job.Scheduler scheduler *job.Scheduler
sseStreamer sse.Streamer
} }
var _ job.Handler = (*Repository)(nil) var _ job.Handler = (*Repository)(nil)
@ -58,7 +61,7 @@ func (r *Repository) Register(executor *job.Executor) error {
// Run starts a background job that imports the provided repository from the provided clone URL. // Run starts a background job that imports the provided repository from the provided clone URL.
func (r *Repository) Run(ctx context.Context, provider Provider, repo *types.Repository, cloneURL string) error { func (r *Repository) Run(ctx context.Context, provider Provider, repo *types.Repository, cloneURL string) error {
jobDef, err := r.getJobDef(*repo.ImportingJobUID, Input{ jobDef, err := r.getJobDef(JobIDFromRepoID(repo.ID), Input{
RepoID: repo.ID, RepoID: repo.ID,
GitUser: provider.Username, GitUser: provider.Username,
GitPass: provider.Password, GitPass: provider.Password,
@ -90,7 +93,7 @@ func (r *Repository) RunMany(ctx context.Context,
repo := repos[k] repo := repos[k]
cloneURL := cloneURLs[k] cloneURL := cloneURLs[k]
jobDef, err := r.getJobDef(*repo.ImportingJobUID, Input{ jobDef, err := r.getJobDef(JobIDFromRepoID(repo.ID), Input{
RepoID: repo.ID, RepoID: repo.ID,
GitUser: provider.Username, GitUser: provider.Username,
GitPass: provider.Password, GitPass: provider.Password,
@ -167,15 +170,13 @@ func (r *Repository) Handle(ctx context.Context, data string, _ job.ProgressRepo
return "", errors.New("missing git repository clone URL") return "", errors.New("missing git repository clone URL")
} }
if input.GitUser != "" || input.GitPass != "" {
repoURL, err := url.Parse(input.CloneURL) repoURL, err := url.Parse(input.CloneURL)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to parse git clone URL: %w", err) return "", fmt.Errorf("failed to parse git clone URL: %w", err)
} }
repoURL.User = url.UserPassword(input.GitUser, input.GitPass) repoURL.User = url.UserPassword(input.GitUser, input.GitPass)
input.CloneURL = repoURL.String() cloneURLWithAuth := repoURL.String()
}
repo, err := r.repoStore.Find(ctx, input.RepoID) repo, err := r.repoStore.Find(ctx, input.RepoID)
if err != nil { if err != nil {
@ -186,23 +187,38 @@ func (r *Repository) Handle(ctx context.Context, data string, _ job.ProgressRepo
return "", fmt.Errorf("repository %s is not being imported", repo.UID) return "", fmt.Errorf("repository %s is not being imported", repo.UID)
} }
log := log.Ctx(ctx).With().
Int64("repo.id", repo.ID).
Str("repo.path", repo.Path).
Logger()
log.Info().Msg("create git repository")
gitUID, err := r.createGitRepository(ctx, &systemPrincipal, repo.ID) gitUID, err := r.createGitRepository(ctx, &systemPrincipal, repo.ID)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to create empty git repository: %w", err) return "", fmt.Errorf("failed to create empty git repository: %w", err)
} }
log.Info().Msgf("successfully created git repository with git_uid '%s'", gitUID)
err = func() error { err = func() error {
repo.GitUID = gitUID repo.GitUID = gitUID
defaultBranch, err := r.syncGitRepository(ctx, &systemPrincipal, repo, input.CloneURL) log.Info().Msg("sync repository")
defaultBranch, err := r.syncGitRepository(ctx, &systemPrincipal, repo, cloneURLWithAuth)
if err != nil { if err != nil {
return fmt.Errorf("failed to sync git repository from '%s': %w", input.CloneURL, err) return fmt.Errorf("failed to sync git repository from '%s': %w", input.CloneURL, err)
} }
log.Info().Msgf("successfully synced repository (returned default branch: '%s')", defaultBranch)
if defaultBranch == "" { if defaultBranch == "" {
defaultBranch = r.defaultBranch defaultBranch = r.defaultBranch
} }
log.Info().Msg("update repo in DB")
repo, err = r.repoStore.UpdateOptLock(ctx, repo, func(repo *types.Repository) error { repo, err = r.repoStore.UpdateOptLock(ctx, repo, func(repo *types.Repository) error {
if !repo.Importing { if !repo.Importing {
return errors.New("repository has already finished importing") return errors.New("repository has already finished importing")
@ -221,25 +237,33 @@ func (r *Repository) Handle(ctx context.Context, data string, _ job.ProgressRepo
return nil return nil
}() }()
if err != nil { if err != nil {
log.Error().Err(err).Msg("failed repository import - cleanup git repository")
if errDel := r.deleteGitRepository(ctx, &systemPrincipal, repo); errDel != nil { if errDel := r.deleteGitRepository(ctx, &systemPrincipal, repo); errDel != nil {
log.Ctx(ctx).Err(err). log.Warn().Err(errDel).
Str("gitUID", gitUID).
Msg("failed to delete git repository after failed import") Msg("failed to delete git repository after failed import")
} }
return "", fmt.Errorf("failed to import repository: %w", err) return "", fmt.Errorf("failed to import repository: %w", err)
} }
err = r.sseStreamer.Publish(ctx, repo.ParentID, enum.SSETypeRepositoryImportCompleted, repo)
if err != nil {
log.Warn().Err(err).Msg("failed to publish import completion SSE")
}
log.Info().Msg("completed repository import")
return "", nil return "", nil
} }
func (r *Repository) GetProgress(ctx context.Context, repo *types.Repository) (types.JobProgress, error) { func (r *Repository) GetProgress(ctx context.Context, repo *types.Repository) (types.JobProgress, error) {
if !repo.Importing || repo.ImportingJobUID == nil || *repo.ImportingJobUID == "" { if !repo.Importing {
// if the repo is not being imported, or it's job ID has been cleared (or never existed) return state=finished // if the repo is not being imported, or it's job ID has been cleared (or never existed) return state=finished
return job.DoneProgress(), nil return job.DoneProgress(), nil
} }
progress, err := r.scheduler.GetJobProgress(ctx, *repo.ImportingJobUID) progress, err := r.scheduler.GetJobProgress(ctx, JobIDFromRepoID(repo.ID))
if errors.Is(err, gitness_store.ErrResourceNotFound) { if errors.Is(err, gitness_store.ErrResourceNotFound) {
// if the job is not found return state=failed // if the job is not found return state=failed
return job.FailProgress(), nil return job.FailProgress(), nil
@ -252,11 +276,11 @@ func (r *Repository) GetProgress(ctx context.Context, repo *types.Repository) (t
} }
func (r *Repository) Cancel(ctx context.Context, repo *types.Repository) error { func (r *Repository) Cancel(ctx context.Context, repo *types.Repository) error {
if repo.ImportingJobUID == nil || *repo.ImportingJobUID == "" { if !repo.Importing {
return nil return nil
} }
err := r.scheduler.CancelJob(ctx, *repo.ImportingJobUID) err := r.scheduler.CancelJob(ctx, JobIDFromRepoID(repo.ID))
if err != nil { if err != nil {
return fmt.Errorf("failed to cancel job: %w", err) return fmt.Errorf("failed to cancel job: %w", err)
} }

View File

@ -8,6 +8,7 @@ import (
"github.com/harness/gitness/encrypt" "github.com/harness/gitness/encrypt"
"github.com/harness/gitness/gitrpc" "github.com/harness/gitness/gitrpc"
"github.com/harness/gitness/internal/services/job" "github.com/harness/gitness/internal/services/job"
"github.com/harness/gitness/internal/sse"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/internal/url" "github.com/harness/gitness/internal/url"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
@ -27,6 +28,7 @@ func ProvideRepoImporter(
encrypter encrypt.Encrypter, encrypter encrypt.Encrypter,
scheduler *job.Scheduler, scheduler *job.Scheduler,
executor *job.Executor, executor *job.Executor,
sseStreamer sse.Streamer,
) (*Repository, error) { ) (*Repository, error) {
importer := &Repository{ importer := &Repository{
defaultBranch: config.Git.DefaultBranch, defaultBranch: config.Git.DefaultBranch,
@ -35,6 +37,7 @@ func ProvideRepoImporter(
repoStore: repoStore, repoStore: repoStore,
encrypter: encrypter, encrypter: encrypter,
scheduler: scheduler, scheduler: scheduler,
sseStreamer: sseStreamer,
} }
err := executor.Register(jobType, importer) err := executor.Register(jobType, importer)

View File

@ -10,6 +10,7 @@ import (
"strings" "strings"
"github.com/harness/gitness/events" "github.com/harness/gitness/events"
"github.com/harness/gitness/internal/bootstrap"
gitevents "github.com/harness/gitness/internal/events/git" gitevents "github.com/harness/gitness/internal/events/git"
"github.com/harness/gitness/internal/pipeline/triggerer" "github.com/harness/gitness/internal/pipeline/triggerer"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
@ -27,6 +28,7 @@ func (s *Service) handleEventBranchCreated(ctx context.Context,
Action: enum.TriggerActionBranchCreated, Action: enum.TriggerActionBranchCreated,
Ref: event.Payload.Ref, Ref: event.Payload.Ref,
Source: ExtractBranch(event.Payload.Ref), Source: ExtractBranch(event.Payload.Ref),
TriggeredBy: bootstrap.NewSystemServiceSession().Principal.ID,
Target: ExtractBranch(event.Payload.Ref), Target: ExtractBranch(event.Payload.Ref),
After: event.Payload.SHA, After: event.Payload.SHA,
} }
@ -45,6 +47,7 @@ func (s *Service) handleEventBranchUpdated(ctx context.Context,
Ref: event.Payload.Ref, Ref: event.Payload.Ref,
Before: event.Payload.OldSHA, Before: event.Payload.OldSHA,
After: event.Payload.NewSHA, After: event.Payload.NewSHA,
TriggeredBy: bootstrap.NewSystemServiceSession().Principal.ID,
Source: ExtractBranch(event.Payload.Ref), Source: ExtractBranch(event.Payload.Ref),
Target: ExtractBranch(event.Payload.Ref), Target: ExtractBranch(event.Payload.Ref),
} }

View File

@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"github.com/harness/gitness/events" "github.com/harness/gitness/events"
"github.com/harness/gitness/internal/bootstrap"
pullreqevents "github.com/harness/gitness/internal/events/pullreq" pullreqevents "github.com/harness/gitness/internal/events/pullreq"
"github.com/harness/gitness/internal/pipeline/triggerer" "github.com/harness/gitness/internal/pipeline/triggerer"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
@ -21,6 +22,7 @@ func (s *Service) handleEventPullReqCreated(ctx context.Context,
hook := &triggerer.Hook{ hook := &triggerer.Hook{
Trigger: enum.TriggerHook, Trigger: enum.TriggerHook,
Action: enum.TriggerActionPullReqCreated, Action: enum.TriggerActionPullReqCreated,
TriggeredBy: bootstrap.NewSystemServiceSession().Principal.ID,
After: event.Payload.SourceSHA, After: event.Payload.SourceSHA,
} }
err := s.augmentPullReqInfo(ctx, hook, event.Payload.PullReqID) err := s.augmentPullReqInfo(ctx, hook, event.Payload.PullReqID)
@ -35,6 +37,7 @@ func (s *Service) handleEventPullReqReopened(ctx context.Context,
hook := &triggerer.Hook{ hook := &triggerer.Hook{
Trigger: enum.TriggerHook, Trigger: enum.TriggerHook,
Action: enum.TriggerActionPullReqReopened, Action: enum.TriggerActionPullReqReopened,
TriggeredBy: bootstrap.NewSystemServiceSession().Principal.ID,
After: event.Payload.SourceSHA, After: event.Payload.SourceSHA,
} }
err := s.augmentPullReqInfo(ctx, hook, event.Payload.PullReqID) err := s.augmentPullReqInfo(ctx, hook, event.Payload.PullReqID)
@ -49,6 +52,7 @@ func (s *Service) handleEventPullReqBranchUpdated(ctx context.Context,
hook := &triggerer.Hook{ hook := &triggerer.Hook{
Trigger: enum.TriggerHook, Trigger: enum.TriggerHook,
Action: enum.TriggerActionPullReqBranchUpdated, Action: enum.TriggerActionPullReqBranchUpdated,
TriggeredBy: bootstrap.NewSystemServiceSession().Principal.ID,
After: event.Payload.NewSHA, After: event.Payload.NewSHA,
} }
err := s.augmentPullReqInfo(ctx, hook, event.Payload.PullReqID) err := s.augmentPullReqInfo(ctx, hook, event.Payload.PullReqID)

View File

@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"github.com/harness/gitness/events" "github.com/harness/gitness/events"
"github.com/harness/gitness/internal/bootstrap"
gitevents "github.com/harness/gitness/internal/events/git" gitevents "github.com/harness/gitness/internal/events/git"
"github.com/harness/gitness/internal/pipeline/triggerer" "github.com/harness/gitness/internal/pipeline/triggerer"
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
@ -19,6 +20,7 @@ func (s *Service) handleEventTagCreated(ctx context.Context,
hook := &triggerer.Hook{ hook := &triggerer.Hook{
Trigger: enum.TriggerHook, Trigger: enum.TriggerHook,
Action: enum.TriggerActionTagCreated, Action: enum.TriggerActionTagCreated,
TriggeredBy: bootstrap.NewSystemServiceSession().Principal.ID,
Ref: event.Payload.Ref, Ref: event.Payload.Ref,
Before: event.Payload.SHA, Before: event.Payload.SHA,
After: event.Payload.SHA, After: event.Payload.SHA,
@ -37,6 +39,7 @@ func (s *Service) handleEventTagUpdated(ctx context.Context,
hook := &triggerer.Hook{ hook := &triggerer.Hook{
Trigger: enum.TriggerHook, Trigger: enum.TriggerHook,
Action: enum.TriggerActionTagUpdated, Action: enum.TriggerActionTagUpdated,
TriggeredBy: bootstrap.NewSystemServiceSession().Principal.ID,
Ref: event.Payload.Ref, Ref: event.Payload.Ref,
Before: event.Payload.OldSHA, Before: event.Payload.OldSHA,
After: event.Payload.NewSHA, After: event.Payload.NewSHA,

View File

@ -113,7 +113,8 @@ func New(
stream.WithConcurrency(config.Concurrency), stream.WithConcurrency(config.Concurrency),
stream.WithHandlerOptions( stream.WithHandlerOptions(
stream.WithIdleTimeout(idleTimeout), stream.WithIdleTimeout(idleTimeout),
stream.WithMaxRetries(config.MaxRetries), // retries not needed for builds which failed to trigger, can be adjusted when needed
stream.WithMaxRetries(0),
)) ))
_ = r.RegisterCreated(service.handleEventPullReqCreated) _ = r.RegisterCreated(service.handleEventPullReqCreated)
@ -156,6 +157,7 @@ func (s *Service) trigger(ctx context.Context, repoID int64,
pipeline, err := s.pipelineStore.Find(ctx, t.PipelineID) pipeline, err := s.pipelineStore.Find(ctx, t.PipelineID)
if err != nil { if err != nil {
errs = multierror.Append(errs, err) errs = multierror.Append(errs, err)
continue
} }
_, err = s.triggerSvc.Trigger(ctx, pipeline, hook) _, err = s.triggerSvc.Trigger(ctx, pipeline, hook)

96
internal/sse/sse.go Normal file
View File

@ -0,0 +1,96 @@
// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
package sse
import (
"context"
"encoding/json"
"fmt"
"strconv"
"github.com/harness/gitness/pubsub"
"github.com/harness/gitness/types/enum"
)
// Event is a server sent event.
type Event struct {
Type enum.SSEType `json:"type"`
Data json.RawMessage `json:"data"`
}
type Streamer interface {
// Publish publishes an event to a given space ID.
Publish(ctx context.Context, spaceID int64, eventType enum.SSEType, data any) error
// Streams streams the events on a space ID.
Stream(ctx context.Context, spaceID int64) (<-chan *Event, <-chan error, func(context.Context) error)
}
type pubsubStreamer struct {
pubsub pubsub.PubSub
namespace string
}
func NewStreamer(pubsub pubsub.PubSub, namespace string) Streamer {
return &pubsubStreamer{
pubsub: pubsub,
namespace: namespace,
}
}
func (e *pubsubStreamer) Publish(ctx context.Context, spaceID int64, eventType enum.SSEType, data any) error {
dataSerialized, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to serialize data: %w", err)
}
event := Event{
Type: eventType,
Data: dataSerialized,
}
serializedEvent, err := json.Marshal(event)
if err != nil {
return fmt.Errorf("failed to serialize event: %w", err)
}
namespaceOption := pubsub.WithPublishNamespace(e.namespace)
topic := getSpaceTopic(spaceID)
err = e.pubsub.Publish(ctx, topic, serializedEvent, namespaceOption)
if err != nil {
return fmt.Errorf("failed to publish event on pubsub: %w", err)
}
return nil
}
func (e *pubsubStreamer) Stream(ctx context.Context, spaceID int64) (<-chan *Event, <-chan error, func(context.Context) error) {
chEvent := make(chan *Event, 100) // TODO: check best size here
chErr := make(chan error)
g := func(payload []byte) error {
event := &Event{}
err := json.Unmarshal(payload, event)
if err != nil {
// This should never happen
return err
}
select {
case chEvent <- event:
default:
}
return nil
}
namespaceOption := pubsub.WithChannelNamespace(e.namespace)
topic := getSpaceTopic(spaceID)
consumer := e.pubsub.Subscribe(ctx, topic, g, namespaceOption)
unsubscribeFN := func(ctx context.Context) error {
return consumer.Unsubscribe(ctx, topic)
}
return chEvent, chErr, unsubscribeFN
}
// getSpaceTopic creates the namespace name which will be `spaces:<id>`
func getSpaceTopic(spaceID int64) string {
return "spaces:" + strconv.Itoa(int(spaceID))
}

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by the Polyform Free Trial License // Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository. // that can be found in the LICENSE.md file for this repository.
package events package sse
import ( import (
"github.com/harness/gitness/pubsub" "github.com/harness/gitness/pubsub"
@ -15,9 +15,7 @@ var WireSet = wire.NewSet(
ProvideEventsStreaming, ProvideEventsStreaming,
) )
func ProvideEventsStreaming(pubsub pubsub.PubSub) EventsStreamer { func ProvideEventsStreaming(pubsub pubsub.PubSub) Streamer {
return &event{ const namespace = "sse"
pubsub: pubsub, return NewStreamer(pubsub, namespace)
topic: "events",
}
} }

View File

@ -560,10 +560,6 @@ type (
// Update tries to update an execution. // Update tries to update an execution.
Update(ctx context.Context, execution *types.Execution) error Update(ctx context.Context, execution *types.Execution) error
// UpdateOptLock updates the execution using the optimistic locking mechanism.
UpdateOptLock(ctx context.Context, execution *types.Execution,
mutateFn func(execution *types.Execution) error) (*types.Execution, error)
// List lists the executions for a given pipeline ID // List lists the executions for a given pipeline ID
List(ctx context.Context, pipelineID int64, pagination types.Pagination) ([]*types.Execution, error) List(ctx context.Context, pipelineID int64, pagination types.Pagination) ([]*types.Execution, error)

View File

@ -38,11 +38,12 @@ type executionStore struct {
type execution struct { type execution struct {
ID int64 `db:"execution_id"` ID int64 `db:"execution_id"`
PipelineID int64 `db:"execution_pipeline_id"` PipelineID int64 `db:"execution_pipeline_id"`
CreatedBy int64 `db:"execution_created_by"`
RepoID int64 `db:"execution_repo_id"` RepoID int64 `db:"execution_repo_id"`
Trigger string `db:"execution_trigger"` Trigger string `db:"execution_trigger"`
Number int64 `db:"execution_number"` Number int64 `db:"execution_number"`
Parent int64 `db:"execution_parent"` Parent int64 `db:"execution_parent"`
Status string `db:"execution_status"` Status enum.CIStatus `db:"execution_status"`
Error string `db:"execution_error"` Error string `db:"execution_error"`
Event string `db:"execution_event"` Event string `db:"execution_event"`
Action string `db:"execution_action"` Action string `db:"execution_action"`
@ -77,6 +78,7 @@ const (
executionColumns = ` executionColumns = `
execution_id execution_id
,execution_pipeline_id ,execution_pipeline_id
,execution_created_by
,execution_repo_id ,execution_repo_id
,execution_trigger ,execution_trigger
,execution_number ,execution_number
@ -149,6 +151,7 @@ func (s *executionStore) Create(ctx context.Context, execution *types.Execution)
INSERT INTO executions ( INSERT INTO executions (
execution_pipeline_id execution_pipeline_id
,execution_repo_id ,execution_repo_id
,execution_created_by
,execution_trigger ,execution_trigger
,execution_number ,execution_number
,execution_parent ,execution_parent
@ -184,6 +187,7 @@ func (s *executionStore) Create(ctx context.Context, execution *types.Execution)
) VALUES ( ) VALUES (
:execution_pipeline_id :execution_pipeline_id
,:execution_repo_id ,:execution_repo_id
,:execution_created_by
,:execution_trigger ,:execution_trigger
,:execution_number ,:execution_number
,:execution_parent ,:execution_parent
@ -284,33 +288,6 @@ func (s *executionStore) Update(ctx context.Context, e *types.Execution) error {
return nil return nil
} }
// UpdateOptLock updates the pipeline using the optimistic locking mechanism.
func (s *executionStore) UpdateOptLock(ctx context.Context,
execution *types.Execution,
mutateFn func(execution *types.Execution) error) (*types.Execution, error) {
for {
dup := *execution
err := mutateFn(&dup)
if err != nil {
return nil, err
}
err = s.Update(ctx, &dup)
if err == nil {
return &dup, nil
}
if !errors.Is(err, gitness_store.ErrVersionConflict) {
return nil, err
}
execution, err = s.FindByNumber(ctx, execution.PipelineID, execution.Number)
if err != nil {
return nil, err
}
}
}
// List lists the executions for a given pipeline ID. // List lists the executions for a given pipeline ID.
// It orders them in descending order of execution number. // It orders them in descending order of execution number.
func (s *executionStore) List( func (s *executionStore) List(

View File

@ -13,6 +13,7 @@ func mapInternalToExecution(in *execution) (*types.Execution, error) {
return &types.Execution{ return &types.Execution{
ID: in.ID, ID: in.ID,
PipelineID: in.PipelineID, PipelineID: in.PipelineID,
CreatedBy: in.CreatedBy,
RepoID: in.RepoID, RepoID: in.RepoID,
Trigger: in.Trigger, Trigger: in.Trigger,
Number: in.Number, Number: in.Number,
@ -53,6 +54,7 @@ func mapExecutionToInternal(in *types.Execution) *execution {
return &execution{ return &execution{
ID: in.ID, ID: in.ID,
PipelineID: in.PipelineID, PipelineID: in.PipelineID,
CreatedBy: in.CreatedBy,
RepoID: in.RepoID, RepoID: in.RepoID,
Trigger: in.Trigger, Trigger: in.Trigger,
Number: in.Number, Number: in.Number,

View File

@ -0,0 +1,3 @@
ALTER TABLE repositories
DROP CONSTRAINT fk_repo_importing_job_uid,
DROP COLUMN repo_importing_job_uid;

View File

@ -0,0 +1,7 @@
ALTER TABLE repositories
ADD COLUMN repo_importing_job_uid TEXT,
ADD CONSTRAINT fk_repo_importing_job_uid
FOREIGN KEY (repo_importing_job_uid)
REFERENCES jobs(job_uid)
ON DELETE SET NULL
ON UPDATE NO ACTION;

View File

@ -0,0 +1,10 @@
DROP TABLE pipelines;
DROP TABLE executions;
DROP TABLE stages;
DROP TABLE secrets;
DROP TABLE steps;
DROP TABLE logs;
DROP TABLE plugins;
DROP TABLE connectors;
DROP TABLE templates;
DROP TABLE triggers;

View File

@ -0,0 +1,206 @@
CREATE TABLE pipelines (
pipeline_id SERIAL PRIMARY KEY,
pipeline_description TEXT NOT NULL,
pipeline_uid TEXT NOT NULL,
pipeline_seq INTEGER NOT NULL DEFAULT 0,
pipeline_disabled BOOLEAN NOT NULL,
pipeline_repo_id INTEGER NOT NULL,
pipeline_default_branch TEXT NOT NULL,
pipeline_created_by INTEGER NOT NULL,
pipeline_config_path TEXT NOT NULL,
pipeline_created BIGINT NOT NULL,
pipeline_updated BIGINT NOT NULL,
pipeline_version INTEGER NOT NULL,
UNIQUE (pipeline_repo_id, pipeline_uid),
CONSTRAINT fk_pipelines_repo_id FOREIGN KEY (pipeline_repo_id)
REFERENCES repositories (repo_id) ON DELETE CASCADE,
CONSTRAINT fk_pipelines_created_by FOREIGN KEY (pipeline_created_by)
REFERENCES principals (principal_id) ON DELETE NO ACTION
);
CREATE TABLE executions (
execution_id SERIAL PRIMARY KEY,
execution_pipeline_id INTEGER NOT NULL,
execution_repo_id INTEGER NOT NULL,
execution_created_by INTEGER NOT NULL,
execution_trigger TEXT NOT NULL,
execution_number INTEGER NOT NULL,
execution_parent INTEGER NOT NULL,
execution_status TEXT NOT NULL,
execution_error TEXT NOT NULL,
execution_event TEXT NOT NULL,
execution_action TEXT NOT NULL,
execution_link TEXT NOT NULL,
execution_timestamp INTEGER NOT NULL,
execution_title TEXT NOT NULL,
execution_message TEXT NOT NULL,
execution_before TEXT NOT NULL,
execution_after TEXT NOT NULL,
execution_ref TEXT NOT NULL,
execution_source_repo TEXT NOT NULL,
execution_source TEXT NOT NULL,
execution_target TEXT NOT NULL,
execution_author TEXT NOT NULL,
execution_author_name TEXT NOT NULL,
execution_author_email TEXT NOT NULL,
execution_author_avatar TEXT NOT NULL,
execution_sender TEXT NOT NULL,
execution_params TEXT NOT NULL,
execution_cron TEXT NOT NULL,
execution_deploy TEXT NOT NULL,
execution_deploy_id INTEGER NOT NULL,
execution_debug BOOLEAN NOT NULL DEFAULT false,
execution_started BIGINT NOT NULL,
execution_finished BIGINT NOT NULL,
execution_created BIGINT NOT NULL,
execution_updated BIGINT NOT NULL,
execution_version INTEGER NOT NULL,
UNIQUE (execution_pipeline_id, execution_number),
CONSTRAINT fk_executions_pipeline_id FOREIGN KEY (execution_pipeline_id)
REFERENCES pipelines (pipeline_id) ON DELETE CASCADE,
CONSTRAINT fk_executions_repo_id FOREIGN KEY (execution_repo_id)
REFERENCES repositories (repo_id) ON DELETE CASCADE,
CONSTRAINT fk_executions_created_by FOREIGN KEY (execution_created_by)
REFERENCES principals (principal_id) ON DELETE NO ACTION
);
CREATE TABLE secrets (
secret_id SERIAL PRIMARY KEY,
secret_uid TEXT NOT NULL,
secret_space_id INTEGER NOT NULL,
secret_description TEXT NOT NULL,
secret_data BYTEA NOT NULL,
secret_created_by INTEGER NOT NULL,
secret_created BIGINT NOT NULL,
secret_updated BIGINT NOT NULL,
secret_version INTEGER NOT NULL,
UNIQUE (secret_space_id, secret_uid),
CONSTRAINT fk_secrets_space_id FOREIGN KEY (secret_space_id)
REFERENCES spaces (space_id) ON DELETE CASCADE,
CONSTRAINT fk_secrets_created_by FOREIGN KEY (secret_created_by)
REFERENCES principals (principal_id) ON DELETE NO ACTION
);
CREATE TABLE stages (
stage_id SERIAL PRIMARY KEY,
stage_execution_id INTEGER NOT NULL,
stage_repo_id INTEGER NOT NULL,
stage_number INTEGER NOT NULL,
stage_kind TEXT NOT NULL,
stage_type TEXT NOT NULL,
stage_name TEXT NOT NULL,
stage_status TEXT NOT NULL,
stage_error TEXT NOT NULL,
stage_parent_group_id INTEGER NOT NULL,
stage_errignore BOOLEAN NOT NULL,
stage_exit_code INTEGER NOT NULL,
stage_limit INTEGER NOT NULL,
stage_os TEXT NOT NULL,
stage_arch TEXT NOT NULL,
stage_variant TEXT NOT NULL,
stage_kernel TEXT NOT NULL,
stage_machine TEXT NOT NULL,
stage_started BIGINT NOT NULL,
stage_stopped BIGINT NOT NULL,
stage_created BIGINT NOT NULL,
stage_updated BIGINT NOT NULL,
stage_version INTEGER NOT NULL,
stage_on_success BOOLEAN NOT NULL,
stage_on_failure BOOLEAN NOT NULL,
stage_depends_on TEXT NOT NULL,
stage_labels TEXT NOT NULL,
stage_limit_repo INTEGER NOT NULL DEFAULT 0,
UNIQUE (stage_execution_id, stage_number),
CONSTRAINT fk_stages_execution_id FOREIGN KEY (stage_execution_id)
REFERENCES executions (execution_id) ON DELETE CASCADE
);
CREATE INDEX ix_stage_in_progress ON stages (stage_status)
WHERE stage_status IN ('pending', 'running');
CREATE TABLE steps (
step_id SERIAL PRIMARY KEY,
step_stage_id INTEGER NOT NULL,
step_number INTEGER NOT NULL,
step_name TEXT NOT NULL,
step_status TEXT NOT NULL,
step_error TEXT NOT NULL,
step_parent_group_id INTEGER NOT NULL,
step_errignore BOOLEAN NOT NULL,
step_exit_code INTEGER NOT NULL,
step_started BIGINT NOT NULL,
step_stopped BIGINT NOT NULL,
step_version INTEGER NOT NULL,
step_depends_on TEXT NOT NULL,
step_image TEXT NOT NULL,
step_detached BOOLEAN NOT NULL,
step_schema TEXT NOT NULL,
UNIQUE (step_stage_id, step_number),
CONSTRAINT fk_steps_stage_id FOREIGN KEY (step_stage_id)
REFERENCES stages (stage_id) ON DELETE CASCADE
);
CREATE TABLE logs (
log_id SERIAL PRIMARY KEY,
log_data BYTEA NOT NULL,
CONSTRAINT fk_logs_id FOREIGN KEY (log_id)
REFERENCES steps (step_id) ON DELETE CASCADE
);
CREATE TABLE connectors (
connector_id SERIAL PRIMARY KEY,
connector_uid TEXT NOT NULL,
connector_description TEXT NOT NULL,
connector_type TEXT NOT NULL,
connector_space_id INTEGER NOT NULL,
connector_data TEXT NOT NULL,
connector_created BIGINT NOT NULL,
connector_updated BIGINT NOT NULL,
connector_version INTEGER NOT NULL,
UNIQUE (connector_space_id, connector_uid),
CONSTRAINT fk_connectors_space_id FOREIGN KEY (connector_space_id)
REFERENCES spaces (space_id) ON DELETE CASCADE
);
CREATE TABLE templates (
template_id SERIAL PRIMARY KEY,
template_uid TEXT NOT NULL,
template_description TEXT NOT NULL,
template_space_id INTEGER NOT NULL,
template_data TEXT NOT NULL,
template_created BIGINT NOT NULL,
template_updated BIGINT NOT NULL,
template_version INTEGER NOT NULL,
UNIQUE (template_space_id, template_uid),
CONSTRAINT fk_templates_space_id FOREIGN KEY (template_space_id)
REFERENCES spaces (space_id) ON DELETE CASCADE
);
CREATE TABLE triggers (
trigger_id SERIAL PRIMARY KEY,
trigger_uid TEXT NOT NULL,
trigger_pipeline_id INTEGER NOT NULL,
trigger_type TEXT NOT NULL,
trigger_repo_id INTEGER NOT NULL,
trigger_secret TEXT NOT NULL,
trigger_description TEXT NOT NULL,
trigger_disabled BOOLEAN NOT NULL,
trigger_created_by INTEGER NOT NULL,
trigger_actions TEXT NOT NULL,
trigger_created BIGINT NOT NULL,
trigger_updated BIGINT NOT NULL,
trigger_version INTEGER NOT NULL,
UNIQUE (trigger_pipeline_id, trigger_uid),
CONSTRAINT fk_triggers_pipeline_id FOREIGN KEY (trigger_pipeline_id)
REFERENCES pipelines (pipeline_id) ON DELETE CASCADE,
CONSTRAINT fk_triggers_repo_id FOREIGN KEY (trigger_repo_id)
REFERENCES repositories (repo_id) ON DELETE CASCADE
);
CREATE TABLE plugins (
plugin_uid TEXT NOT NULL,
plugin_description TEXT NOT NULL,
plugin_logo TEXT NOT NULL,
plugin_spec BYTEA NOT NULL,
UNIQUE (plugin_uid)
);

View File

@ -0,0 +1 @@
ALTER TABLE repositories ADD COLUMN repo_importing_job_uid TEXT;

View File

@ -0,0 +1 @@
ALTER TABLE repositories DROP COLUMN repo_importing_job_uid;

View File

@ -0,0 +1,10 @@
DROP TABLE pipelines;
DROP TABLE executions;
DROP TABLE stages;
DROP TABLE secrets;
DROP TABLE steps;
DROP TABLE logs;
DROP TABLE plugins;
DROP TABLE connectors;
DROP TABLE templates;
DROP TABLE triggers;

View File

@ -1,20 +1,12 @@
DROP TABLE IF exists pipelines;
DROP TABLE IF exists executions;
DROP TABLE IF exists stages;
DROP TABLE IF exists secrets;
DROP TABLE IF exists steps;
DROP TABLE IF exists logs;
DROP TABLE IF exists plugins;
DROP TABLE IF exists connectors;
DROP TABLE IF exists templates;
DROP TABLE IF exists triggers;
CREATE TABLE pipelines ( CREATE TABLE pipelines (
pipeline_id INTEGER PRIMARY KEY AUTOINCREMENT pipeline_id INTEGER PRIMARY KEY AUTOINCREMENT
,pipeline_description TEXT NOT NULL ,pipeline_description TEXT NOT NULL
,pipeline_uid TEXT NOT NULL ,pipeline_uid TEXT NOT NULL
,pipeline_seq INTEGER NOT NULL DEFAULT 0 ,pipeline_seq INTEGER NOT NULL DEFAULT 0
,pipeline_disabled BOOLEAN NOT NULL
,pipeline_repo_id INTEGER NOT NULL ,pipeline_repo_id INTEGER NOT NULL
,pipeline_default_branch TEXT NOT NULL ,pipeline_default_branch TEXT NOT NULL
,pipeline_created_by INTEGER NOT NULL
,pipeline_config_path TEXT NOT NULL ,pipeline_config_path TEXT NOT NULL
,pipeline_created INTEGER NOT NULL ,pipeline_created INTEGER NOT NULL
,pipeline_updated INTEGER NOT NULL ,pipeline_updated INTEGER NOT NULL
@ -28,12 +20,19 @@ CREATE TABLE pipelines (
REFERENCES repositories (repo_id) MATCH SIMPLE REFERENCES repositories (repo_id) MATCH SIMPLE
ON UPDATE NO ACTION ON UPDATE NO ACTION
ON DELETE CASCADE ON DELETE CASCADE
-- Foreign key to principals table
,CONSTRAINT fk_pipelines_created_by FOREIGN KEY (pipeline_created_by)
REFERENCES principals (principal_id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
); );
CREATE TABLE executions ( CREATE TABLE executions (
execution_id INTEGER PRIMARY KEY AUTOINCREMENT execution_id INTEGER PRIMARY KEY AUTOINCREMENT
,execution_pipeline_id INTEGER NOT NULL ,execution_pipeline_id INTEGER NOT NULL
,execution_repo_id INTEGER NOT NULL ,execution_repo_id INTEGER NOT NULL
,execution_created_by INTEGER NOT NULL
,execution_trigger TEXT NOT NULL ,execution_trigger TEXT NOT NULL
,execution_number INTEGER NOT NULL ,execution_number INTEGER NOT NULL
,execution_parent INTEGER NOT NULL ,execution_parent INTEGER NOT NULL
@ -81,6 +80,12 @@ CREATE TABLE executions (
REFERENCES repositories (repo_id) MATCH SIMPLE REFERENCES repositories (repo_id) MATCH SIMPLE
ON UPDATE NO ACTION ON UPDATE NO ACTION
ON DELETE CASCADE ON DELETE CASCADE
-- Foreign key to principals table
,CONSTRAINT fk_executions_created_by FOREIGN KEY (execution_created_by)
REFERENCES principals (principal_id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
); );
CREATE TABLE secrets ( CREATE TABLE secrets (
@ -92,6 +97,7 @@ CREATE TABLE secrets (
,secret_created INTEGER NOT NULL ,secret_created INTEGER NOT NULL
,secret_updated INTEGER NOT NULL ,secret_updated INTEGER NOT NULL
,secret_version INTEGER NOT NULL ,secret_version INTEGER NOT NULL
,secret_created_by INTEGER NOT NULL
-- Ensure unique combination of space ID and UID -- Ensure unique combination of space ID and UID
,UNIQUE (secret_space_id, secret_uid) ,UNIQUE (secret_space_id, secret_uid)
@ -101,6 +107,12 @@ CREATE TABLE secrets (
REFERENCES spaces (space_id) MATCH SIMPLE REFERENCES spaces (space_id) MATCH SIMPLE
ON UPDATE NO ACTION ON UPDATE NO ACTION
ON DELETE CASCADE ON DELETE CASCADE
-- Foreign key to principals table
,CONSTRAINT fk_secrets_created_by FOREIGN KEY (secret_created_by)
REFERENCES principals (principal_id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
); );
CREATE TABLE stages ( CREATE TABLE stages (
@ -189,34 +201,6 @@ CREATE TABLE logs (
ON DELETE CASCADE ON DELETE CASCADE
); );
-- Insert some pipelines
INSERT INTO pipelines (
pipeline_id, pipeline_description, pipeline_uid, pipeline_seq,
pipeline_repo_id, pipeline_default_branch,
pipeline_config_path, pipeline_created, pipeline_updated, pipeline_version
) VALUES (
1, 'Sample Pipeline 1', 'pipeline_uid_1', 2, 1,
'main', 'config_path_1', 1678932000, 1678932100, 1
);
INSERT INTO pipelines (
pipeline_id, pipeline_description, pipeline_uid, pipeline_seq,
pipeline_repo_id, pipeline_default_branch,
pipeline_config_path, pipeline_created, pipeline_updated, pipeline_version
) VALUES (
2, 'Sample Pipeline 2', 'pipeline_uid_2', 0, 1,
'develop', 'config_path_2', 1678932200, 1678932300, 1
);
INSERT INTO pipelines (
pipeline_id, pipeline_description, pipeline_uid, pipeline_seq,
pipeline_repo_id, pipeline_default_branch,
pipeline_config_path, pipeline_created, pipeline_updated, pipeline_version
) VALUES (
3, 'Sample Pipeline 3', 'pipeline_uid_3', 0, 1,
'develop', 'config_path_2', 1678932200000, 1678932300000, 1
);
CREATE TABLE connectors ( CREATE TABLE connectors (
connector_id INTEGER PRIMARY KEY AUTOINCREMENT connector_id INTEGER PRIMARY KEY AUTOINCREMENT
,connector_uid TEXT NOT NULL ,connector_uid TEXT NOT NULL
@ -262,10 +246,11 @@ CREATE TABLE triggers (
trigger_id INTEGER PRIMARY KEY AUTOINCREMENT trigger_id INTEGER PRIMARY KEY AUTOINCREMENT
,trigger_uid TEXT NOT NULL ,trigger_uid TEXT NOT NULL
,trigger_pipeline_id INTEGER NOT NULL ,trigger_pipeline_id INTEGER NOT NULL
,trigger_type TEXT NOT NULL
,trigger_repo_id INTEGER NOT NULL ,trigger_repo_id INTEGER NOT NULL
,trigger_secret TEXT NOT NULL ,trigger_secret TEXT NOT NULL
,trigger_description TEXT NOT NULL ,trigger_description TEXT NOT NULL
,trigger_enabled BOOLEAN NOT NULL ,trigger_disabled BOOLEAN NOT NULL
,trigger_created_by INTEGER NOT NULL ,trigger_created_by INTEGER NOT NULL
,trigger_actions TEXT NOT NULL ,trigger_actions TEXT NOT NULL
,trigger_created INTEGER NOT NULL ,trigger_created INTEGER NOT NULL
@ -297,19 +282,3 @@ CREATE TABLE plugins (
-- Ensure unique plugin names -- Ensure unique plugin names
,UNIQUE(plugin_uid) ,UNIQUE(plugin_uid)
); );
INSERT INTO plugins (plugin_uid, plugin_description, plugin_logo, plugin_spec)
VALUES
('plugins/slack', 'A sample slack plugin', 'slack.png',
'inputs:
channel:
type: string
token:
type: string
steps:
- type: script
spec:
image: plugins/slack
envs:
PLUGIN_CHANNEL: <+ inputs.channel >');

View File

@ -31,6 +31,7 @@ const (
pipelineColumns = ` pipelineColumns = `
pipeline_id pipeline_id
,pipeline_description ,pipeline_description
,pipeline_created_by
,pipeline_uid ,pipeline_uid
,pipeline_seq ,pipeline_seq
,pipeline_repo_id ,pipeline_repo_id
@ -87,6 +88,8 @@ func (s *pipelineStore) Create(ctx context.Context, pipeline *types.Pipeline) er
,pipeline_uid ,pipeline_uid
,pipeline_seq ,pipeline_seq
,pipeline_repo_id ,pipeline_repo_id
,pipeline_disabled
,pipeline_created_by
,pipeline_default_branch ,pipeline_default_branch
,pipeline_config_path ,pipeline_config_path
,pipeline_created ,pipeline_created
@ -97,6 +100,8 @@ func (s *pipelineStore) Create(ctx context.Context, pipeline *types.Pipeline) er
:pipeline_uid, :pipeline_uid,
:pipeline_seq, :pipeline_seq,
:pipeline_repo_id, :pipeline_repo_id,
:pipeline_disabled,
:pipeline_created_by,
:pipeline_default_branch, :pipeline_default_branch,
:pipeline_config_path, :pipeline_config_path,
:pipeline_created, :pipeline_created,
@ -229,7 +234,7 @@ func (s *pipelineStore) ListLatest(
` `
// Create a subquery to get max execution IDs for each unique execution pipeline ID. // Create a subquery to get max execution IDs for each unique execution pipeline ID.
subquery := database.Builder. subquery := database.Builder.
Select("execution_pipeline_id, execution_id, MAX(execution_number)"). Select("execution_pipeline_id, MAX(execution_id) AS execution_id").
From("executions"). From("executions").
Where("execution_repo_id = ?"). Where("execution_repo_id = ?").
GroupBy("execution_pipeline_id") GroupBy("execution_pipeline_id")

View File

@ -8,6 +8,7 @@ import (
"database/sql" "database/sql"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
) )
// pipelineExecutionjoin struct represents a joined row between pipelines and executions // pipelineExecutionjoin struct represents a joined row between pipelines and executions
@ -15,6 +16,7 @@ type pipelineExecutionJoin struct {
*types.Pipeline *types.Pipeline
ID sql.NullInt64 `db:"execution_id"` ID sql.NullInt64 `db:"execution_id"`
PipelineID sql.NullInt64 `db:"execution_pipeline_id"` PipelineID sql.NullInt64 `db:"execution_pipeline_id"`
Action sql.NullString `db:"execution_action"`
Message sql.NullString `db:"execution_message"` Message sql.NullString `db:"execution_message"`
After sql.NullString `db:"execution_after"` After sql.NullString `db:"execution_after"`
RepoID sql.NullInt64 `db:"execution_repo_id"` RepoID sql.NullInt64 `db:"execution_repo_id"`
@ -56,11 +58,12 @@ func convertPipelineJoin(join *pipelineExecutionJoin) *types.Pipeline {
ID: join.ID.Int64, ID: join.ID.Int64,
PipelineID: join.PipelineID.Int64, PipelineID: join.PipelineID.Int64,
RepoID: join.RepoID.Int64, RepoID: join.RepoID.Int64,
Action: join.Action.String,
Trigger: join.Trigger.String, Trigger: join.Trigger.String,
Number: join.Number.Int64, Number: join.Number.Int64,
After: join.After.String, After: join.After.String,
Message: join.Message.String, Message: join.Message.String,
Status: join.Status.String, Status: enum.ParseCIStatus(join.Status.String),
Error: join.Error.String, Error: join.Error.String,
Link: join.Link.String, Link: join.Link.String,
Timestamp: join.Timestamp.Int64, Timestamp: join.Timestamp.Int64,

View File

@ -59,8 +59,7 @@ const (
,repo_num_closed_pulls ,repo_num_closed_pulls
,repo_num_open_pulls ,repo_num_open_pulls
,repo_num_merged_pulls ,repo_num_merged_pulls
,repo_importing ,repo_importing`
,repo_importing_job_uid`
repoSelectBaseWithJoin = ` repoSelectBaseWithJoin = `
SELECT` + repoColumnsForJoin + ` SELECT` + repoColumnsForJoin + `
@ -127,7 +126,6 @@ func (s *RepoStore) Create(ctx context.Context, repo *types.Repository) error {
,repo_num_open_pulls ,repo_num_open_pulls
,repo_num_merged_pulls ,repo_num_merged_pulls
,repo_importing ,repo_importing
,repo_importing_job_uid
) values ( ) values (
:repo_version :repo_version
,:repo_parent_id ,:repo_parent_id
@ -147,7 +145,6 @@ func (s *RepoStore) Create(ctx context.Context, repo *types.Repository) error {
,:repo_num_open_pulls ,:repo_num_open_pulls
,:repo_num_merged_pulls ,:repo_num_merged_pulls
,:repo_importing ,:repo_importing
,:repo_importing_job_uid
) RETURNING repo_id` ) RETURNING repo_id`
db := dbtx.GetAccessor(ctx, s.db) db := dbtx.GetAccessor(ctx, s.db)
@ -185,7 +182,6 @@ func (s *RepoStore) Update(ctx context.Context, repo *types.Repository) error {
,repo_num_open_pulls = :repo_num_open_pulls ,repo_num_open_pulls = :repo_num_open_pulls
,repo_num_merged_pulls = :repo_num_merged_pulls ,repo_num_merged_pulls = :repo_num_merged_pulls
,repo_importing = :repo_importing ,repo_importing = :repo_importing
,repo_importing_job_uid = :repo_importing_job_uid
WHERE repo_id = :repo_id AND repo_version = :repo_version - 1` WHERE repo_id = :repo_id AND repo_version = :repo_version - 1`
updatedAt := time.Now() updatedAt := time.Now()

View File

@ -31,6 +31,7 @@ const (
secret_id, secret_id,
secret_description, secret_description,
secret_space_id, secret_space_id,
secret_created_by,
secret_uid, secret_uid,
secret_data, secret_data,
secret_created, secret_created,
@ -82,6 +83,7 @@ func (s *secretStore) Create(ctx context.Context, secret *types.Secret) error {
INSERT INTO secrets ( INSERT INTO secrets (
secret_description, secret_description,
secret_space_id, secret_space_id,
secret_created_by,
secret_uid, secret_uid,
secret_data, secret_data,
secret_created, secret_created,
@ -90,6 +92,7 @@ func (s *secretStore) Create(ctx context.Context, secret *types.Secret) error {
) VALUES ( ) VALUES (
:secret_description, :secret_description,
:secret_space_id, :secret_space_id,
:secret_created_by,
:secret_uid, :secret_uid,
:secret_data, :secret_data,
:secret_created, :secret_created,

View File

@ -14,6 +14,7 @@ import (
"github.com/harness/gitness/store/database" "github.com/harness/gitness/store/database"
"github.com/harness/gitness/store/database/dbtx" "github.com/harness/gitness/store/database/dbtx"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
sqlxtypes "github.com/jmoiron/sqlx/types" sqlxtypes "github.com/jmoiron/sqlx/types"
@ -61,7 +62,7 @@ type stage struct {
Name string `db:"stage_name"` Name string `db:"stage_name"`
Kind string `db:"stage_kind"` Kind string `db:"stage_kind"`
Type string `db:"stage_type"` Type string `db:"stage_type"`
Status string `db:"stage_status"` Status enum.CIStatus `db:"stage_status"`
Error string `db:"stage_error"` Error string `db:"stage_error"`
ParentGroupID int64 `db:"stage_parent_group_id"` ParentGroupID int64 `db:"stage_parent_group_id"`
ErrIgnore bool `db:"stage_errignore"` ErrIgnore bool `db:"stage_errignore"`
@ -265,9 +266,17 @@ func (s *stageStore) Update(ctx context.Context, st *types.Stage) error {
SET SET
stage_status = :stage_status stage_status = :stage_status
,stage_machine = :stage_machine ,stage_machine = :stage_machine
,stage_started = :stage_started
,stage_stopped = :stage_stopped
,stage_exit_code = :stage_exit_code
,stage_updated = :stage_updated ,stage_updated = :stage_updated
,stage_version = :stage_version ,stage_version = :stage_version
,stage_error = :stage_error ,stage_error = :stage_error
,stage_on_success = :stage_on_success
,stage_on_failure = :stage_on_failure
,stage_errignore = :stage_errignore
,stage_depends_on = :stage_depends_on
,stage_labels = :stage_labels
WHERE stage_id = :stage_id AND stage_version = :stage_version - 1` WHERE stage_id = :stage_id AND stage_version = :stage_version - 1`
updatedAt := time.Now() updatedAt := time.Now()
steps := st.Steps steps := st.Steps

View File

@ -10,6 +10,7 @@ import (
"fmt" "fmt"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
sqlxtypes "github.com/jmoiron/sqlx/types" sqlxtypes "github.com/jmoiron/sqlx/types"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -46,7 +47,7 @@ func convertFromNullStep(nullstep *nullstep) (*types.Step, error) {
StageID: nullstep.StageID.Int64, StageID: nullstep.StageID.Int64,
Number: nullstep.Number.Int64, Number: nullstep.Number.Int64,
Name: nullstep.Name.String, Name: nullstep.Name.String,
Status: nullstep.Status.String, Status: enum.ParseCIStatus(nullstep.Status.String),
Error: nullstep.Error.String, Error: nullstep.Error.String,
ErrIgnore: nullstep.ErrIgnore.Bool, ErrIgnore: nullstep.ErrIgnore.Bool,
ExitCode: int(nullstep.ExitCode.Int64), ExitCode: int(nullstep.ExitCode.Int64),

View File

@ -13,6 +13,7 @@ import (
"github.com/harness/gitness/store/database" "github.com/harness/gitness/store/database"
"github.com/harness/gitness/store/database/dbtx" "github.com/harness/gitness/store/database/dbtx"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
sqlxtypes "github.com/jmoiron/sqlx/types" sqlxtypes "github.com/jmoiron/sqlx/types"
@ -46,7 +47,7 @@ type step struct {
Number int64 `db:"step_number"` Number int64 `db:"step_number"`
ParentGroupID int64 `db:"step_parent_group_id"` ParentGroupID int64 `db:"step_parent_group_id"`
Name string `db:"step_name"` Name string `db:"step_name"`
Status string `db:"step_status"` Status enum.CIStatus `db:"step_status"`
Error string `db:"step_error"` Error string `db:"step_error"`
ErrIgnore bool `db:"step_errignore"` ErrIgnore bool `db:"step_errignore"`
ExitCode int `db:"step_exit_code"` ExitCode int `db:"step_exit_code"`

View File

@ -29,11 +29,12 @@ type trigger struct {
ID int64 `db:"trigger_id"` ID int64 `db:"trigger_id"`
UID string `db:"trigger_uid"` UID string `db:"trigger_uid"`
Description string `db:"trigger_description"` Description string `db:"trigger_description"`
Type string `db:"trigger_type"`
Secret string `db:"trigger_secret"` Secret string `db:"trigger_secret"`
PipelineID int64 `db:"trigger_pipeline_id"` PipelineID int64 `db:"trigger_pipeline_id"`
RepoID int64 `db:"trigger_repo_id"` RepoID int64 `db:"trigger_repo_id"`
CreatedBy int64 `db:"trigger_created_by"` CreatedBy int64 `db:"trigger_created_by"`
Enabled bool `db:"trigger_enabled"` Disabled bool `db:"trigger_disabled"`
Actions sqlxtypes.JSONText `db:"trigger_actions"` Actions sqlxtypes.JSONText `db:"trigger_actions"`
Created int64 `db:"trigger_created"` Created int64 `db:"trigger_created"`
Updated int64 `db:"trigger_updated"` Updated int64 `db:"trigger_updated"`
@ -50,11 +51,12 @@ func mapInternalToTrigger(trigger *trigger) (*types.Trigger, error) {
return &types.Trigger{ return &types.Trigger{
ID: trigger.ID, ID: trigger.ID,
Description: trigger.Description, Description: trigger.Description,
Type: trigger.Type,
Secret: trigger.Secret, Secret: trigger.Secret,
PipelineID: trigger.PipelineID, PipelineID: trigger.PipelineID,
RepoID: trigger.RepoID, RepoID: trigger.RepoID,
CreatedBy: trigger.CreatedBy, CreatedBy: trigger.CreatedBy,
Enabled: trigger.Enabled, Disabled: trigger.Disabled,
Actions: actions, Actions: actions,
UID: trigger.UID, UID: trigger.UID,
Created: trigger.Created, Created: trigger.Created,
@ -80,11 +82,12 @@ func mapTriggerToInternal(t *types.Trigger) *trigger {
ID: t.ID, ID: t.ID,
UID: t.UID, UID: t.UID,
Description: t.Description, Description: t.Description,
Type: t.Type,
PipelineID: t.PipelineID, PipelineID: t.PipelineID,
Secret: t.Secret, Secret: t.Secret,
RepoID: t.RepoID, RepoID: t.RepoID,
CreatedBy: t.CreatedBy, CreatedBy: t.CreatedBy,
Enabled: t.Enabled, Disabled: t.Disabled,
Actions: EncodeToSQLXJSON(t.Actions), Actions: EncodeToSQLXJSON(t.Actions),
Created: t.Created, Created: t.Created,
Updated: t.Updated, Updated: t.Updated,
@ -107,7 +110,7 @@ const (
triggerColumns = ` triggerColumns = `
trigger_id trigger_id
,trigger_uid ,trigger_uid
,trigger_enabled ,trigger_disabled
,trigger_actions ,trigger_actions
,trigger_description ,trigger_description
,trigger_pipeline_id ,trigger_pipeline_id
@ -139,7 +142,8 @@ func (s *triggerStore) Create(ctx context.Context, t *types.Trigger) error {
trigger_uid trigger_uid
,trigger_description ,trigger_description
,trigger_actions ,trigger_actions
,trigger_enabled ,trigger_disabled
,trigger_type
,trigger_secret ,trigger_secret
,trigger_created_by ,trigger_created_by
,trigger_pipeline_id ,trigger_pipeline_id
@ -151,7 +155,8 @@ func (s *triggerStore) Create(ctx context.Context, t *types.Trigger) error {
:trigger_uid :trigger_uid
,:trigger_description ,:trigger_description
,:trigger_actions ,:trigger_actions
,:trigger_enabled ,:trigger_disabled
,:trigger_type
,:trigger_secret ,:trigger_secret
,:trigger_created_by ,:trigger_created_by
,:trigger_pipeline_id ,:trigger_pipeline_id
@ -286,7 +291,7 @@ func (s *triggerStore) ListAllEnabled(
stmt := database.Builder. stmt := database.Builder.
Select(triggerColumns). Select(triggerColumns).
From("triggers"). From("triggers").
Where("trigger_repo_id = ? AND trigger_enabled = true", fmt.Sprint(repoID)) Where("trigger_repo_id = ? AND trigger_disabled = false", fmt.Sprint(repoID))
sql, args, err := stmt.ToSql() sql, args, err := stmt.ToSql()
if err != nil { if err != nil {

View File

@ -98,9 +98,12 @@ func (r *InMemory) Publish(ctx context.Context, topic string, payload []byte, op
} }
topic = formatTopic(pubConfig.app, pubConfig.namespace, topic) topic = formatTopic(pubConfig.app, pubConfig.namespace, topic)
wg := sync.WaitGroup{}
for _, sub := range r.registry { for _, sub := range r.registry {
if slices.Contains(sub.topics, topic) && !sub.isClosed() { if slices.Contains(sub.topics, topic) && !sub.isClosed() {
wg.Add(1)
go func(subscriber *inMemorySubscriber) { go func(subscriber *inMemorySubscriber) {
defer wg.Done()
// timer is based on subscriber data // timer is based on subscriber data
t := time.NewTimer(subscriber.config.sendTimeout) t := time.NewTimer(subscriber.config.sendTimeout)
defer t.Stop() defer t.Stop()
@ -118,6 +121,10 @@ func (r *InMemory) Publish(ctx context.Context, topic string, payload []byte, op
} }
} }
// Wait for all subscribers to complete
// Otherwise, we might fail notifying some subscribers due to context completion.
wg.Wait()
return nil return nil
} }

View File

@ -53,3 +53,11 @@ type ReqCheck struct {
type CheckPayloadText struct { type CheckPayloadText struct {
Details string `json:"details"` Details string `json:"details"`
} }
// CheckPayloadInternal is for internal use for more seamless integration for
// gitness CI status checks.
type CheckPayloadInternal struct {
Number int64 `json:"execution_number"`
RepoID int64 `json:"repo_id"`
PipelineID int64 `json:"pipeline_id"`
}

View File

@ -44,10 +44,12 @@ const (
CheckPayloadKindEmpty CheckPayloadKind = "" CheckPayloadKindEmpty CheckPayloadKind = ""
CheckPayloadKindRaw CheckPayloadKind = "raw" CheckPayloadKindRaw CheckPayloadKind = "raw"
CheckPayloadKindMarkdown CheckPayloadKind = "markdown" CheckPayloadKindMarkdown CheckPayloadKind = "markdown"
CheckPayloadKindPipeline CheckPayloadKind = "pipeline"
) )
var checkPayloadTypes = sortEnum([]CheckPayloadKind{ var checkPayloadTypes = sortEnum([]CheckPayloadKind{
CheckPayloadKindEmpty, CheckPayloadKindEmpty,
CheckPayloadKindRaw, CheckPayloadKindRaw,
CheckPayloadKindMarkdown, CheckPayloadKindMarkdown,
CheckPayloadKindPipeline,
}) })

View File

@ -5,15 +5,66 @@
// Status types for CI. // Status types for CI.
package enum package enum
const ( import (
CIStatusSkipped = "skipped" "strings"
CIStatusBlocked = "blocked"
CIStatusDeclined = "declined"
CIStatusWaitingOnDeps = "waiting_on_dependencies"
CIStatusPending = "pending"
CIStatusRunning = "running"
CIStatusSuccess = "success"
CIStatusFailure = "failure"
CIStatusKilled = "killed"
CIStatusError = "error"
) )
// CIStatus defines the different kinds of CI statuses for
// stages, steps and executions.
type CIStatus string
const (
CIStatusSkipped CIStatus = "skipped"
CIStatusBlocked CIStatus = "blocked"
CIStatusDeclined CIStatus = "declined"
CIStatusWaitingOnDeps CIStatus = "waiting_on_dependencies"
CIStatusPending CIStatus = "pending"
CIStatusRunning CIStatus = "running"
CIStatusSuccess CIStatus = "success"
CIStatusFailure CIStatus = "failure"
CIStatusKilled CIStatus = "killed"
CIStatusError CIStatus = "error"
)
func (status CIStatus) ConvertToCheckStatus() CheckStatus {
if status == CIStatusPending || status == CIStatusWaitingOnDeps {
return CheckStatusPending
}
if status == CIStatusSuccess || status == CIStatusSkipped {
return CheckStatusSuccess
}
if status == CIStatusFailure {
return CheckStatusFailure
}
if status == CIStatusRunning {
return CheckStatusRunning
}
return CheckStatusError
}
// ParseCIStatus converts the status from a string to typed enum.
// If the match is not exact, will just return default error status
// instead of explicitly returning not found error.
func ParseCIStatus(status string) CIStatus {
switch strings.ToLower(status) {
case "skipped", "blocked", "declined", "waiting_on_dependencies", "pending", "running", "success", "failure", "killed", "error":
return CIStatus(strings.ToLower(status))
case "": // just in case status is not passed through
return CIStatusPending
default:
return CIStatusError
}
}
// IsDone returns true if the build has a completed state.
func (status CIStatus) IsDone() bool {
switch status {
case CIStatusWaitingOnDeps,
CIStatusPending,
CIStatusRunning,
CIStatusBlocked:
return false
default:
return true
}
}

View File

@ -1,15 +0,0 @@
// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
// Enums for event types delivered to the event stream for the UI
package enum
// EventType defines the kind of event
type EventType string
const (
ExecutionUpdated = "execution_updated"
ExecutionRunning = "execution_running"
ExecutionCompleted = "execution_completed"
)

18
types/enum/sse.go Normal file
View File

@ -0,0 +1,18 @@
// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
// Enums for event types delivered to the event stream for the UI
package enum
// SSEType defines the kind of server sent event
type SSEType string
const (
SSETypeExecutionUpdated = "execution_updated"
SSETypeExecutionRunning = "execution_running"
SSETypeExecutionCompleted = "execution_completed"
SSETypeExecutionCanceled = "execution_canceled"
SSETypeRepositoryImportCompleted = "repository_import_completed"
)

View File

@ -4,15 +4,18 @@
package types package types
import "github.com/harness/gitness/types/enum"
// Execution represents an instance of a pipeline execution. // Execution represents an instance of a pipeline execution.
type Execution struct { type Execution struct {
ID int64 `json:"-"` ID int64 `json:"-"`
PipelineID int64 `json:"pipeline_id"` PipelineID int64 `json:"pipeline_id"`
CreatedBy int64 `json:"created_by"`
RepoID int64 `json:"repo_id"` RepoID int64 `json:"repo_id"`
Trigger string `json:"trigger,omitempty"` Trigger string `json:"trigger,omitempty"`
Number int64 `json:"number"` Number int64 `json:"number"`
Parent int64 `json:"parent,omitempty"` Parent int64 `json:"parent,omitempty"`
Status string `json:"status"` Status enum.CIStatus `json:"status"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
Event string `json:"event,omitempty"` Event string `json:"event,omitempty"`
Action string `json:"action,omitempty"` Action string `json:"action,omitempty"`

View File

@ -8,6 +8,8 @@ type Pipeline struct {
ID int64 `db:"pipeline_id" json:"id"` ID int64 `db:"pipeline_id" json:"id"`
Description string `db:"pipeline_description" json:"description"` Description string `db:"pipeline_description" json:"description"`
UID string `db:"pipeline_uid" json:"uid"` UID string `db:"pipeline_uid" json:"uid"`
Disabled bool `db:"pipeline_disabled" json:"disabled"`
CreatedBy int64 `db:"pipeline_created_by" json:"created_by"`
Seq int64 `db:"pipeline_seq" json:"seq"` // last execution number for this pipeline Seq int64 `db:"pipeline_seq" json:"seq"` // last execution number for this pipeline
RepoID int64 `db:"pipeline_repo_id" json:"repo_id"` RepoID int64 `db:"pipeline_repo_id" json:"repo_id"`
DefaultBranch string `db:"pipeline_default_branch" json:"default_branch"` DefaultBranch string `db:"pipeline_default_branch" json:"default_branch"`

View File

@ -28,7 +28,6 @@ type Repository struct {
ForkID int64 `db:"repo_fork_id" json:"fork_id"` ForkID int64 `db:"repo_fork_id" json:"fork_id"`
PullReqSeq int64 `db:"repo_pullreq_seq" json:"-"` PullReqSeq int64 `db:"repo_pullreq_seq" json:"-"`
// TODO: Check if we want to keep those values here
NumForks int `db:"repo_num_forks" json:"num_forks"` NumForks int `db:"repo_num_forks" json:"num_forks"`
NumPulls int `db:"repo_num_pulls" json:"num_pulls"` NumPulls int `db:"repo_num_pulls" json:"num_pulls"`
NumClosedPulls int `db:"repo_num_closed_pulls" json:"num_closed_pulls"` NumClosedPulls int `db:"repo_num_closed_pulls" json:"num_closed_pulls"`
@ -36,7 +35,6 @@ type Repository struct {
NumMergedPulls int `db:"repo_num_merged_pulls" json:"num_merged_pulls"` NumMergedPulls int `db:"repo_num_merged_pulls" json:"num_merged_pulls"`
Importing bool `db:"repo_importing" json:"importing"` Importing bool `db:"repo_importing" json:"importing"`
ImportingJobUID *string `db:"repo_importing_job_uid" json:"-"`
// git urls // git urls
GitURL string `db:"-" json:"git_url"` GitURL string `db:"-" json:"git_url"`

View File

@ -8,6 +8,7 @@ type Secret struct {
ID int64 `db:"secret_id" json:"id"` ID int64 `db:"secret_id" json:"id"`
Description string `db:"secret_description" json:"description"` Description string `db:"secret_description" json:"description"`
SpaceID int64 `db:"secret_space_id" json:"space_id"` SpaceID int64 `db:"secret_space_id" json:"space_id"`
CreatedBy int64 `db:"secret_created_by" json:"created_by"`
UID string `db:"secret_uid" json:"uid"` UID string `db:"secret_uid" json:"uid"`
Data string `db:"secret_data" json:"-"` Data string `db:"secret_data" json:"-"`
Created int64 `db:"secret_created" json:"created"` Created int64 `db:"secret_created" json:"created"`

View File

@ -4,6 +4,8 @@
package types package types
import "github.com/harness/gitness/types/enum"
type Stage struct { type Stage struct {
ID int64 `json:"-"` ID int64 `json:"-"`
ExecutionID int64 `json:"execution_id"` ExecutionID int64 `json:"execution_id"`
@ -12,7 +14,7 @@ type Stage struct {
Name string `json:"name"` Name string `json:"name"`
Kind string `json:"kind,omitempty"` Kind string `json:"kind,omitempty"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
Status string `json:"status"` Status enum.CIStatus `json:"status"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
ErrIgnore bool `json:"errignore,omitempty"` ErrIgnore bool `json:"errignore,omitempty"`
ExitCode int `json:"exit_code"` ExitCode int `json:"exit_code"`

View File

@ -4,12 +4,14 @@
package types package types
import "github.com/harness/gitness/types/enum"
type Step struct { type Step struct {
ID int64 `json:"-"` ID int64 `json:"-"`
StageID int64 `json:"-"` StageID int64 `json:"-"`
Number int64 `json:"number"` Number int64 `json:"number"`
Name string `json:"name"` Name string `json:"name"`
Status string `json:"status"` Status enum.CIStatus `json:"status"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
ErrIgnore bool `json:"errignore,omitempty"` ErrIgnore bool `json:"errignore,omitempty"`
ExitCode int `json:"exit_code"` ExitCode int `json:"exit_code"`

View File

@ -9,11 +9,12 @@ import "github.com/harness/gitness/types/enum"
type Trigger struct { type Trigger struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Description string `json:"description"` Description string `json:"description"`
Type string `json:"trigger_type"`
PipelineID int64 `json:"pipeline_id"` PipelineID int64 `json:"pipeline_id"`
Secret string `json:"-"` Secret string `json:"-"`
RepoID int64 `json:"repo_id"` RepoID int64 `json:"repo_id"`
CreatedBy int64 `json:"created_by"` CreatedBy int64 `json:"created_by"`
Enabled bool `json:"enabled"` Disabled bool `json:"disabled"`
Actions []enum.TriggerAction `json:"actions"` Actions []enum.TriggerAction `json:"actions"`
UID string `json:"uid"` UID string `json:"uid"`
Created int64 `json:"created"` Created int64 `json:"created"`

View File

@ -101,10 +101,6 @@ rules:
- lodash.* - lodash.*
paths: paths:
- lodash - lodash
- name: yaml
importNames:
- stringify
message: 'Please use yamlStringify from @common/utils/YamlHelperMethods instead of this'
overrides: overrides:
- files: - files:

View File

@ -47,6 +47,7 @@ export interface CODERoutes {
toCODESpaceSettings: (args: Required<Pick<CODEProps, 'space'>>) => string toCODESpaceSettings: (args: Required<Pick<CODEProps, 'space'>>) => string
toCODEPipelines: (args: Required<Pick<CODEProps, 'repoPath'>>) => string toCODEPipelines: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
toCODEPipelineEdit: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string toCODEPipelineEdit: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string
toCODEPipelineSettings: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string
toCODESecrets: (args: Required<Pick<CODEProps, 'space'>>) => string toCODESecrets: (args: Required<Pick<CODEProps, 'space'>>) => string
toCODEGlobalSettings: () => string toCODEGlobalSettings: () => string
@ -98,6 +99,7 @@ export const routes: CODERoutes = {
toCODESpaceSettings: ({ space }) => `/settings/${space}`, toCODESpaceSettings: ({ space }) => `/settings/${space}`,
toCODEPipelines: ({ repoPath }) => `/${repoPath}/pipelines`, toCODEPipelines: ({ repoPath }) => `/${repoPath}/pipelines`,
toCODEPipelineEdit: ({ repoPath, pipeline }) => `/${repoPath}/pipelines/${pipeline}/edit`, toCODEPipelineEdit: ({ repoPath, pipeline }) => `/${repoPath}/pipelines/${pipeline}/edit`,
toCODEPipelineSettings: ({ repoPath, pipeline }) => `/${repoPath}/pipelines/${pipeline}/triggers`,
toCODESecrets: ({ space }) => `/secrets/${space}`, toCODESecrets: ({ space }) => `/secrets/${space}`,
toCODEGlobalSettings: () => '/settings', toCODEGlobalSettings: () => '/settings',

View File

@ -33,6 +33,7 @@ import Secret from 'pages/Secret/Secret'
import Search from 'pages/Search/Search' import Search from 'pages/Search/Search'
import AddUpdatePipeline from 'pages/AddUpdatePipeline/AddUpdatePipeline' import AddUpdatePipeline from 'pages/AddUpdatePipeline/AddUpdatePipeline'
import { useAppContext } from 'AppContext' import { useAppContext } from 'AppContext'
import PipelineSettings from 'components/PipelineSettings/PipelineSettings'
export const RouteDestinations: React.FC = React.memo(function RouteDestinations() { export const RouteDestinations: React.FC = React.memo(function RouteDestinations() {
const { getString } = useStrings() const { getString } = useStrings()
@ -194,6 +195,14 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
</Route> </Route>
)} )}
{standalone && (
<Route path={routes.toCODEPipelineSettings({ repoPath, pipeline: pathProps.pipeline })} exact>
<LayoutWithSideNav title={getString('pageTitle.pipelines')}>
<PipelineSettings />
</LayoutWithSideNav>
</Route>
)}
{standalone && ( {standalone && (
<Route path={routes.toCODEPipelines({ repoPath })} exact> <Route path={routes.toCODEPipelines({ repoPath })} exact>
<LayoutWithSideNav title={getString('pageTitle.pipelines')}> <LayoutWithSideNav title={getString('pageTitle.pipelines')}>

View File

@ -36,7 +36,7 @@ const CloneCredentialDialog = (props: CloneCredentialDialogProps) => {
}, },
[mutate, showError] [mutate, showError]
) )
const tokenData = standalone ? false : hooks?.useGenerateToken?.(hash, currentUser.uid, flag) const tokenData = standalone ? false : hooks?.useGenerateToken?.(hash, currentUser?.uid, flag)
useEffect(() => { useEffect(() => {
if (tokenData) { if (tokenData) {

View File

@ -47,7 +47,6 @@ export const Editor = React.memo(function CodeMirrorReactEditor({
onViewUpdate, onViewUpdate,
darkTheme darkTheme
}: EditorProps) { }: EditorProps) {
const contentRef = useRef(content)
const view = useRef<EditorView>() const view = useRef<EditorView>()
const ref = useRef<HTMLDivElement>() const ref = useRef<HTMLDivElement>()
const languageConfig = useMemo(() => new Compartment(), []) const languageConfig = useMemo(() => new Compartment(), [])
@ -139,14 +138,5 @@ export const Editor = React.memo(function CodeMirrorReactEditor({
} }
}, [filename, forMarkdown, view, languageConfig, markdownLanguageSupport]) }, [filename, forMarkdown, view, languageConfig, markdownLanguageSupport])
useEffect(() => {
if (contentRef.current !== content) {
contentRef.current = content
viewRef?.current?.dispatch({
changes: { from: 0, to: viewRef?.current?.state.doc.length, insert: content }
})
}
}, [content, viewRef])
return <Container ref={ref} className={cx(css.editor, className)} style={style} /> return <Container ref={ref} className={cx(css.editor, className)} style={style} />
}) })

View File

@ -10,7 +10,7 @@ import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import type { CODEProps } from 'RouteDefinitions' import type { CODEProps } from 'RouteDefinitions'
import type { GitInfoProps } from 'utils/GitUtils' import type { GitInfoProps } from 'utils/GitUtils'
import { ExecutionStatus } from 'components/ExecutionStatus/ExecutionStatus' import { ExecutionStatus } from 'components/ExecutionStatus/ExecutionStatus'
import { getStatus } from 'utils/PipelineUtils' import { getStatus } from 'utils/ExecutionUtils'
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator' import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
import { timeDistance } from 'utils/Utils' import { timeDistance } from 'utils/Utils'
import css from './ExecutionPageHeader.module.scss' import css from './ExecutionPageHeader.module.scss'

View File

@ -3,7 +3,7 @@ import { Container, FlexExpander, Layout, Text } from '@harnessio/uicore'
import cx from 'classnames' import cx from 'classnames'
import type { TypesStage } from 'services/code' import type { TypesStage } from 'services/code'
import { ExecutionState, ExecutionStatus } from 'components/ExecutionStatus/ExecutionStatus' import { ExecutionState, ExecutionStatus } from 'components/ExecutionStatus/ExecutionStatus'
import { getStatus } from 'utils/PipelineUtils' import { getStatus } from 'utils/ExecutionUtils'
import { timeDistance } from 'utils/Utils' import { timeDistance } from 'utils/Utils'
import css from './ExecutionStageList.module.scss' import css from './ExecutionStageList.module.scss'

View File

@ -0,0 +1,189 @@
import React, { useState } from 'react'
import { Intent } from '@blueprintjs/core'
import * as yup from 'yup'
import { Button, Container, Layout, FlexExpander, Formik, FormikForm, FormInput, Text } from '@harnessio/uicore'
import { Icon } from '@harnessio/icons'
import { FontVariation } from '@harnessio/design-system'
import { useStrings } from 'framework/strings'
import { REGEX_VALID_REPO_NAME } from 'utils/Utils'
import { ImportFormData, RepoVisibility, parseUrl } from 'utils/GitUtils'
import css from '../NewRepoModalButton.module.scss'
interface ImportFormProps {
handleSubmit: (data: ImportFormData) => void
loading: boolean // eslint-disable-next-line @typescript-eslint/no-explicit-any
hideModal: any
}
const ImportForm = (props: ImportFormProps) => {
const { handleSubmit, loading, hideModal } = props
const { getString } = useStrings()
const [auth, setAuth] = useState(false)
// eslint-disable-next-line no-control-regex
const MATCH_REPOURL_REGEX = /^(https?:\/\/(?:www\.)?(github|gitlab)\.com\/([^/]+\/[^/]+))/
const formInitialValues: ImportFormData = {
repoUrl: '',
username: '',
password: '',
name: '',
description: '',
isPublic: RepoVisibility.PRIVATE
}
return (
<Formik
initialValues={formInitialValues}
formName="importRepoForm"
validationSchema={yup.object().shape({
repoUrl: yup
.string()
.matches(MATCH_REPOURL_REGEX, getString('importSpace.invalidUrl'))
.required(getString('importRepo.required')),
name: yup
.string()
.trim()
.required(getString('validation.nameIsRequired'))
.matches(REGEX_VALID_REPO_NAME, getString('validation.repoNamePatternIsNotValid'))
})}
onSubmit={handleSubmit}>
{formik => {
return (
<FormikForm>
<FormInput.Text
name="repoUrl"
label={getString('importRepo.url')}
placeholder={getString('importRepo.urlPlaceholder')}
tooltipProps={{
dataTooltipId: 'repositoryURLTextField'
}}
onChange={event => {
const target = event.target as HTMLInputElement
formik.setFieldValue('repoUrl', target.value)
if (target.value) {
const provider = parseUrl(target.value)
if (provider?.fullRepo) {
formik.setFieldValue('name', provider.repoName ? provider.repoName : provider?.fullRepo)
formik.validateField('repoUrl')
}
}
}}
/>
<FormInput.CheckBox
name="authorization"
label={getString('importRepo.reqAuth')}
tooltipProps={{
dataTooltipId: 'authorization'
}}
onClick={() => {
setAuth(!auth)
}}
/>
{auth ? (
<>
<FormInput.Text
name="username"
label={getString('userName')}
placeholder={getString('importRepo.userPlaceholder')}
tooltipProps={{
dataTooltipId: 'repositoryUserTextField'
}}
/>
<FormInput.Text
inputGroup={{ type: 'password' }}
name="password"
label={getString('importRepo.passToken')}
placeholder={getString('importRepo.passwordPlaceholder')}
tooltipProps={{
dataTooltipId: 'repositoryPasswordTextField'
}}
/>
</>
) : null}
<hr className={css.dividerContainer} />
<FormInput.Text
name="name"
label={getString('name')}
placeholder={getString('enterRepoName')}
tooltipProps={{
dataTooltipId: 'repositoryNameTextField'
}}
onChange={() => {
formik.validateField('repoUrl')
}}
/>
<FormInput.Text
name="description"
label={getString('description')}
placeholder={getString('enterDescription')}
tooltipProps={{
dataTooltipId: 'repositoryDescriptionTextField'
}}
/>
<hr className={css.dividerContainer} />
<Container>
<FormInput.RadioGroup
name="isPublic"
label=""
items={[
{
label: (
<Container>
<Layout.Horizontal>
<Icon name="git-clone-step" size={20} margin={{ right: 'medium' }} />
<Container>
<Layout.Vertical spacing="xsmall">
<Text>{getString('public')}</Text>
<Text font={{ variation: FontVariation.TINY }}>
{getString('createRepoModal.publicLabel')}
</Text>
</Layout.Vertical>
</Container>
</Layout.Horizontal>
</Container>
),
value: RepoVisibility.PUBLIC
},
{
label: (
<Container>
<Layout.Horizontal>
<Icon name="git-clone-step" size={20} margin={{ right: 'medium' }} />
<Container margin={{ left: 'small' }}>
<Layout.Vertical spacing="xsmall">
<Text>{getString('private')}</Text>
<Text font={{ variation: FontVariation.TINY }}>
{getString('createRepoModal.privateLabel')}
</Text>
</Layout.Vertical>
</Container>
</Layout.Horizontal>
</Container>
),
value: RepoVisibility.PRIVATE
}
]}
/>
</Container>
<Layout.Horizontal
spacing="small"
padding={{ right: 'xxlarge', top: 'xlarge', bottom: 'large' }}
style={{ alignItems: 'center' }}>
<Button type="submit" text={getString('importRepo.title')} intent={Intent.PRIMARY} disabled={loading} />
<Button text={getString('cancel')} minimal onClick={hideModal} />
<FlexExpander />
{loading && <Icon intent={Intent.PRIMARY} name="steps-spinner" size={16} />}
</Layout.Horizontal>
</FormikForm>
)
}}
</Formik>
)
}
export default ImportForm

View File

@ -1,3 +1,29 @@
.divider { .divider {
margin: var(--spacing-medium) 0 var(--spacing-large) 0 !important; margin: var(--spacing-medium) 0 var(--spacing-large) 0 !important;
} }
.dividerContainer {
opacity: 0.2;
height: 1px;
color: var(--grey-100);
margin: 20px 0;
}
.popover {
transform: translateY(5px) !important;
.menuItem {
strong {
display: inline-block;
margin-left: 10px;
}
p {
font-size: 12px;
padding: 0 var(--spacing-xlarge);
line-height: 16px;
margin: 5px 0;
max-width: 320px;
}
}
}

View File

@ -1,3 +1,6 @@
/* eslint-disable */ /* eslint-disable */
// This is an auto-generated file // This is an auto-generated file
export declare const divider: string export declare const divider: string
export declare const dividerContainer: string
export declare const menuItem: string
export declare const popover: string

View File

@ -1,5 +1,14 @@
import React, { useEffect, useMemo, useState } from 'react' import React, { useEffect, useMemo, useState } from 'react'
import { Icon as BPIcon, Classes, Dialog, Intent, Menu, MenuDivider, MenuItem } from '@blueprintjs/core' import {
Icon as BPIcon,
Classes,
Dialog,
Intent,
Menu,
MenuDivider,
MenuItem,
PopoverPosition
} from '@blueprintjs/core'
import * as yup from 'yup' import * as yup from 'yup'
import { import {
Button, Button,
@ -15,7 +24,8 @@ import {
Text, Text,
ButtonVariation, ButtonVariation,
ButtonSize, ButtonSize,
TextInput TextInput,
SplitButton
} from '@harnessio/uicore' } from '@harnessio/uicore'
import { Icon } from '@harnessio/icons' import { Icon } from '@harnessio/icons'
import { FontVariation } from '@harnessio/design-system' import { FontVariation } from '@harnessio/design-system'
@ -30,26 +40,19 @@ import {
REGEX_VALID_REPO_NAME, REGEX_VALID_REPO_NAME,
SUGGESTED_BRANCH_NAMES SUGGESTED_BRANCH_NAMES
} from 'utils/Utils' } from 'utils/Utils'
import { isGitBranchNameValid } from 'utils/GitUtils' import {
ImportFormData,
RepoCreationType,
RepoFormData,
RepoVisibility,
isGitBranchNameValid,
parseUrl
} from 'utils/GitUtils'
import type { TypesRepository, OpenapiCreateRepositoryRequest } from 'services/code' import type { TypesRepository, OpenapiCreateRepositoryRequest } from 'services/code'
import { useAppContext } from 'AppContext' import { useAppContext } from 'AppContext'
import ImportForm from './ImportForm/ImportForm'
import css from './NewRepoModalButton.module.scss' import css from './NewRepoModalButton.module.scss'
enum RepoVisibility {
PUBLIC = 'public',
PRIVATE = 'private'
}
interface RepoFormData {
name: string
description: string
license: string
defaultBranch: string
gitignore: string
addReadme: boolean
isPublic: RepoVisibility
}
const formInitialValues: RepoFormData = { const formInitialValues: RepoFormData = {
name: '', name: '',
description: '', description: '',
@ -90,6 +93,15 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
space_path: space space_path: space
} }
}) })
const { mutate: importRepo, loading: importRepoLoading } = useMutate<TypesRepository>({
verb: 'POST',
path: `/api/v1/repos/import`,
queryParams: standalone
? undefined
: {
space_path: space
}
})
const { const {
data: gitignores, data: gitignores,
loading: gitIgnoreLoading, loading: gitIgnoreLoading,
@ -100,14 +112,13 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
loading: licenseLoading, loading: licenseLoading,
error: licenseError error: licenseError
} = useGet({ path: '/api/v1/resources/license' }) } = useGet({ path: '/api/v1/resources/license' })
const loading = submitLoading || gitIgnoreLoading || licenseLoading const loading = submitLoading || gitIgnoreLoading || licenseLoading || importRepoLoading
useEffect(() => { useEffect(() => {
if (gitIgnoreError || licenseError) { if (gitIgnoreError || licenseError) {
showError(getErrorMessage(gitIgnoreError || licenseError), 0) showError(getErrorMessage(gitIgnoreError || licenseError), 0)
} }
}, [gitIgnoreError, licenseError, showError]) }, [gitIgnoreError, licenseError, showError])
const handleSubmit = (formData: RepoFormData) => { const handleSubmit = (formData: RepoFormData) => {
try { try {
const payload: OpenapiCreateRepositoryRequest = { const payload: OpenapiCreateRepositoryRequest = {
@ -132,7 +143,24 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
showError(getErrorMessage(exception), 0, getString('failedToCreateRepo')) showError(getErrorMessage(exception), 0, getString('failedToCreateRepo'))
} }
} }
const handleImportSubmit = (formData: ImportFormData) => {
const provider = parseUrl(formData.repoUrl)
const importPayload = {
description: formData.description || '',
parent_ref: space,
uid: formData.name,
provider: { type: provider?.provider.toLowerCase(), username: formData.username, password: formData.password },
provider_repo: provider?.fullRepo
}
importRepo(importPayload)
.then(response => {
hideModal()
onSubmit(response)
})
.catch(_error => {
showError(getErrorMessage(_error), 0, getString('importRepo.failedToImportRepo'))
})
}
return ( return (
<Dialog <Dialog
isOpen isOpen
@ -145,10 +173,13 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
style={{ height: '100%' }} style={{ height: '100%' }}
data-testid="add-target-to-flag-modal"> data-testid="add-target-to-flag-modal">
<Heading level={3} font={{ variation: FontVariation.H3 }} margin={{ bottom: 'xlarge' }}> <Heading level={3} font={{ variation: FontVariation.H3 }} margin={{ bottom: 'xlarge' }}>
{modalTitle} {repoOption.type === RepoCreationType.IMPORT ? getString('importRepo.title') : modalTitle}
</Heading> </Heading>
<Container margin={{ right: 'xxlarge' }}> <Container margin={{ right: 'xxlarge' }}>
{repoOption.type === RepoCreationType.IMPORT ? (
<ImportForm hideModal={hideModal} handleSubmit={handleImportSubmit} loading={false} />
) : (
<Formik <Formik
initialValues={formInitialValues} initialValues={formInitialValues}
formName="editVariations" formName="editVariations"
@ -279,13 +310,29 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
</Layout.Horizontal> </Layout.Horizontal>
</FormikForm> </FormikForm>
</Formik> </Formik>
)}
</Container> </Container>
</Layout.Vertical> </Layout.Vertical>
</Dialog> </Dialog>
) )
} }
const { getString } = useStrings()
const [openModal, hideModal] = useModalHook(ModalComponent, [onSubmit]) const repoCreateOptions: RepoCreationOption[] = [
{
type: RepoCreationType.CREATE,
title: getString('newRepo'),
desc: getString('createNewRepo')
},
{
type: RepoCreationType.IMPORT,
title: getString('importGitRepo'),
desc: getString('importGitRepo')
}
]
const [repoOption, setRepoOption] = useState<RepoCreationOption>(repoCreateOptions[0])
const [openModal, hideModal] = useModalHook(ModalComponent, [onSubmit, repoOption])
const { standalone } = useAppContext() const { standalone } = useAppContext()
const { hooks } = useAppContext() const { hooks } = useAppContext()
const permResult = hooks?.usePermissionTranslate?.( const permResult = hooks?.usePermissionTranslate?.(
@ -297,8 +344,48 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
}, },
[space] [space]
) )
return (
<SplitButton
{...props}
loading={false}
text={repoOption.title}
variation={ButtonVariation.PRIMARY}
popoverProps={{
interactionKind: 'click',
usePortal: true,
popoverClassName: css.popover,
position: PopoverPosition.BOTTOM_RIGHT,
transitionDuration: 1000
}}
icon={repoOption.type === RepoCreationType.IMPORT ? undefined : 'plus'}
{...permissionProps(permResult, standalone)}
onClick={() => {
openModal()
}}>
{repoCreateOptions.map(option => {
return (
<Menu.Item
key={option.type}
className={css.menuItem}
text={
<>
<p>{option.desc}</p>
</>
}
onClick={() => {
setRepoOption(option)
}}
/>
)
})}
</SplitButton>
)
}
return <Button onClick={openModal} {...props} {...permissionProps(permResult, standalone)} /> interface RepoCreationOption {
type: RepoCreationType
title: string
desc: string
} }
interface BranchNameProps { interface BranchNameProps {

View File

@ -21,7 +21,7 @@ import { useStrings } from 'framework/strings'
import type { OpenapiCreateSecretRequest, TypesSecret } from 'services/code' import type { OpenapiCreateSecretRequest, TypesSecret } from 'services/code'
import { getErrorMessage } from 'utils/Utils' import { getErrorMessage } from 'utils/Utils'
interface SecretFormData { export interface SecretFormData {
value: string value: string
description: string description: string
name: string name: string
@ -82,10 +82,7 @@ export const NewSecretModalButton: React.FC<NewSecretModalButtonProps> = ({
onClose={hideModal} onClose={hideModal}
title={''} title={''}
style={{ width: 700, maxHeight: '95vh', overflow: 'auto' }}> style={{ width: 700, maxHeight: '95vh', overflow: 'auto' }}>
<Layout.Vertical <Layout.Vertical padding={{ left: 'xxlarge' }} style={{ height: '100%' }} data-testid="add-secret-modal">
padding={{ left: 'xxlarge' }}
style={{ height: '100%' }}
data-testid="add-target-to-flag-modal">
<Heading level={3} font={{ variation: FontVariation.H3 }} margin={{ bottom: 'xlarge' }}> <Heading level={3} font={{ variation: FontVariation.H3 }} margin={{ bottom: 'xlarge' }}>
{modalTitle} {modalTitle}
</Heading> </Heading>

View File

@ -0,0 +1,316 @@
import React, { useState } from 'react'
import { Intent } from '@blueprintjs/core'
import * as yup from 'yup'
import { Color } from '@harnessio/design-system'
import { Button, Container, Label, Layout, FlexExpander, Formik, FormikForm, FormInput, Text } from '@harnessio/uicore'
import { Icon } from '@harnessio/icons'
import { useStrings } from 'framework/strings'
import { Organization, type ImportSpaceFormData } from 'utils/GitUtils'
import css from '../NewSpaceModalButton.module.scss'
interface ImportFormProps {
handleSubmit: (data: ImportSpaceFormData) => void
loading: boolean
// eslint-disable-next-line @typescript-eslint/no-explicit-any
hideModal: any
}
const ImportSpaceForm = (props: ImportFormProps) => {
const { handleSubmit, loading, hideModal } = props
const { getString } = useStrings()
const [auth, setAuth] = useState(false)
const [step, setStep] = useState(0)
const [buttonLoading, setButtonLoading] = useState(false)
const formInitialValues: ImportSpaceFormData = {
gitProvider: '',
username: '',
password: '',
name: '',
description: '',
organization: ''
}
const providers = [
{ value: 'Github', label: 'Github' },
{ value: 'Gitlab', label: 'Gitlab' }
]
const validationSchemaStepOne = yup.object().shape({
gitProvider: yup.string().trim().required(getString('importSpace.providerRequired'))
})
const validationSchemaStepTwo = yup.object().shape({
organization: yup.string().trim().required(getString('importSpace.orgRequired')),
name: yup.string().trim().required(getString('importSpace.spaceNameRequired'))
})
return (
<Formik
initialValues={formInitialValues}
formName="importSpaceForm"
enableReinitialize={true}
validateOnBlur
onSubmit={handleSubmit}>
{formik => {
const { values } = formik
const handleValidationClick = async () => {
try {
if (step === 0) {
await validationSchemaStepOne.validate(formik.values, { abortEarly: false })
setStep(1)
} else if (step === 1) {
await validationSchemaStepTwo.validate(formik.values, { abortEarly: false })
setButtonLoading(true)
} // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
formik.setErrors(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
err.inner.reduce((acc: { [x: string]: any }, current: { path: string | number; message: string }) => {
acc[current.path] = current.message
return acc
}, {})
)
}
}
const handleImport = async () => {
await handleSubmit(formik.values)
setButtonLoading(false)
}
return (
<Container width={'97%'}>
<FormikForm>
{step === 0 ? (
<>
<Container width={'70%'}>
<Layout.Horizontal>
<Icon className={css.icon} name="code-info" size={16} />
<Text padding={{ left: 'small' }} font={{ size: 'small' }}>
{getString('importSpace.content')}
</Text>
</Layout.Horizontal>
</Container>
<hr className={css.dividerContainer} />
<Container className={css.textContainer} width={'70%'}>
<FormInput.Select
value={{ value: values.gitProvider, label: values.gitProvider } || providers[0]}
name={'gitProvider'}
label={getString('importSpace.gitProvider')}
items={providers}
className={css.selectBox}
/>
{formik.errors.gitProvider ? (
<Text
margin={{ top: 'small', bottom: 'small' }}
color={Color.RED_500}
icon="circle-cross"
iconProps={{ color: Color.RED_500 }}>
{formik.errors.gitProvider}
</Text>
) : null}
<Layout.Horizontal flex>
{getString('importSpace.authorization')}
<Container padding={{ left: 'small' }} width={'100%'}>
<hr className={css.dividerContainer} />
</Container>
</Layout.Horizontal>
<FormInput.Text
name="username"
label={getString('userName')}
placeholder={getString('importRepo.userPlaceholder')}
tooltipProps={{
dataTooltipId: 'spaceUserTextField'
}}
/>
{formik.errors.username ? (
<Text
margin={{ top: 'small', bottom: 'small' }}
color={Color.RED_500}
icon="circle-cross"
iconProps={{ color: Color.RED_500 }}>
{formik.errors.username}
</Text>
) : null}
<FormInput.Text
name="password"
label={getString('importRepo.passToken')}
placeholder={getString('importRepo.passwordPlaceholder')}
tooltipProps={{
dataTooltipId: 'spacePasswordTextField'
}}
inputGroup={{ type: 'password' }}
/>
{formik.errors.password ? (
<Text
margin={{ top: 'small', bottom: 'small' }}
color={Color.RED_500}
icon="circle-cross"
iconProps={{ color: Color.RED_500 }}>
{formik.errors.password}
</Text>
) : null}
</Container>
</>
) : null}
{step === 1 ? (
<>
<Layout.Horizontal flex>
{/* <Container className={css.detailsLabel}> */}
<Text className={css.detailsLabel} font={{ size: 'small' }} flex>
{getString('importSpace.details')}
</Text>
{/* </Container> */}
<Container padding={{ left: 'small' }} width={'100%'}>
<hr className={css.dividerContainer} />
</Container>
</Layout.Horizontal>
<Container className={css.textContainer} width={'70%'}>
<FormInput.Text
name="organization"
label={
formik.values.gitProvider === Organization.GITHUB
? getString('importSpace.githubOrg')
: getString('importSpace.gitlabGroup')
}
placeholder={getString('importSpace.orgNamePlaceholder')}
tooltipProps={{
dataTooltipId: 'importSpaceOrgName'
}}
/>
{formik.errors.organization ? (
<Text
margin={{ bottom: 'small' }}
color={Color.RED_500}
icon="circle-cross"
iconProps={{ color: Color.RED_500 }}>
{formik.errors.organization}
</Text>
) : null}
<Layout.Horizontal>
<Label>{getString('importSpace.importLabel')}</Label>
<Icon padding={{ left: 'small' }} className={css.icon} name="code-info" size={16} />
</Layout.Horizontal>
<Container className={css.importContainer} padding={'medium'}>
<Layout.Horizontal>
<FormInput.CheckBox
name="repositories"
label={getString('pageTitle.repositories')}
tooltipProps={{
dataTooltipId: 'authorization'
}}
defaultChecked
onClick={() => {
setAuth(!auth)
}}
disabled
padding={{ right: 'small' }}
className={css.checkbox}
/>
<Container padding={{ left: 'xxxlarge' }}>
<FormInput.CheckBox
name="pipelines"
label={getString('pageTitle.pipelines')}
tooltipProps={{
dataTooltipId: 'pipelines'
}}
onClick={() => {
setAuth(!auth)
}}
/>
</Container>
</Layout.Horizontal>
</Container>
<Container>
<hr className={css.dividerContainer} />
<FormInput.Text
name="name"
label={getString('importSpace.spaceName')}
placeholder={getString('enterName')}
tooltipProps={{
dataTooltipId: 'importSpaceName'
}}
/>
{formik.errors.name ? (
<Text
margin={{ bottom: 'small' }}
color={Color.RED_500}
icon="circle-cross"
iconProps={{ color: Color.RED_500 }}>
{formik.errors.name}
</Text>
) : null}
<FormInput.Text
name="description"
label={getString('importSpace.description')}
placeholder={getString('importSpace.descPlaceholder')}
tooltipProps={{
dataTooltipId: 'importSpaceDesc'
}}
/>
</Container>
</Container>
</>
) : null}
<hr className={css.dividerContainer} />
<Layout.Horizontal
spacing="small"
padding={{ right: 'xxlarge', bottom: 'large' }}
style={{ alignItems: 'center' }}>
{step === 1 ? (
<Button
disabled={buttonLoading}
text={
buttonLoading ? (
<>
<Container className={css.loadingIcon} width={93.5} flex={{ alignItems: 'center' }}>
<Icon className={css.loadingIcon} name="steps-spinner" size={16} />
</Container>
</>
) : (
getString('importSpace.title')
)
}
intent={Intent.PRIMARY}
onClick={() => {
handleValidationClick()
if (formik.values.name !== '' || formik.values.organization !== '') {
handleImport()
setButtonLoading(false)
}
formik.setErrors({})
}}
/>
) : (
<Button
text={getString('importSpace.next')}
intent={Intent.PRIMARY}
onClick={() => {
handleValidationClick()
if (
(!formik.errors.gitProvider && formik.touched.gitProvider) ||
(!formik.errors.username && formik.touched.username) ||
(!formik.errors.password && formik.touched.password)
) {
formik.setErrors({})
setStep(1)
}
}}
/>
)}
<Button text={getString('cancel')} minimal onClick={hideModal} />
<FlexExpander />
{loading && <Icon intent={Intent.PRIMARY} name="steps-spinner" size={16} />}
</Layout.Horizontal>
</FormikForm>
</Container>
)
}}
</Formik>
)
}
export default ImportSpaceForm

View File

@ -1,3 +1,127 @@
.divider { .divider {
margin: var(--spacing-medium) 0 var(--spacing-large) 0 !important; margin: var(--spacing-medium) 0 var(--spacing-large) 0 !important;
} }
.popoverSplit {
:global {
.bp3-menu {
min-width: 248px;
max-width: 248px;
}
.bp3-menu-item {
min-width: 240px;
max-width: 240px;
}
.menuItem {
max-width: fit-content;
}
}
}
.test {
padding: 20px;
}
.dividerContainer {
opacity: 0.2;
height: 1px;
color: var(--grey-100);
margin: 20px 0;
}
.dividerContainer {
opacity: 0.2;
height: 1px;
color: var(--grey-100);
margin: 20px 0;
}
.halfDividerContainer {
opacity: 0.2;
height: 1px;
color: var(--grey-100);
margin-bottom: 20px;
}
.textContainer {
font-size: var(--font-size-small) !important;
--form-input-font-size: var(--font-size-small) !important;
.selectBox {
margin-bottom: unset !important;
}
}
.menuItem {
max-width: fit-content;
}
.importContainer {
background: var(--grey-50) !important;
border: 1px solid var(--grey-200) !important;
border-radius: 4px;
:global {
.bp3-form-group {
margin: unset !important;
}
}
}
.loadingIcon {
fill: var(--grey-0) !important;
color: var(--grey-0) !important;
:global {
.bp3-icon {
padding-left: 45px !important;
fill: var(--grey-0) !important;
color: var(--grey-0) !important;
}
}
}
.detailsLabel {
white-space: nowrap !important;
max-width: 155px !important;
color: var(--grey-600) !important;
}
.icon {
> svg {
fill: var(--primary-7) !important;
> path {
fill: var(--primary-7) !important;
}
}
}
.checkbox {
:global {
.Tooltip--acenter {
opacity: 0.7 !important;
}
.bp3-control-indicator {
background: var(--primary-7) !important;
opacity: 0.7;
}
}
}
.popoverSpace {
position: relative;
left: 33px;
:global {
.bp3-menu {
min-width: 162px;
max-width: 162px;
}
.bp3-menu-item {
min-width: 153px;
max-width: 153px;
}
.menuItem {
max-width: fit-content;
}
}
}

Some files were not shown because too many files have changed in this diff Show More