feat: [AH-396]: webhook support (#2778)

* feat: [AH-396]: resolve PR comments
* feat: [AH-396]: adjust sql
* feat: [AH-396]: implement registry webhooks
This commit is contained in:
Tudor Macari 2025-01-23 17:22:35 +00:00 committed by Harness
parent 49e52935c4
commit a655c2f8e9
29 changed files with 3454 additions and 86 deletions

View File

@ -0,0 +1,4 @@
DROP INDEX registry_webhooks_registry_id_identifier;
DROP INDEX registry_webhooks_space_id_identifier;
DROP TABLE registry_webhook_executions;
DROP TABLE registry_webhooks;

View File

@ -0,0 +1,61 @@
CREATE TABLE registry_webhooks
(
registry_webhook_id SERIAL PRIMARY KEY,
registry_webhook_version INTEGER NOT NULL DEFAULT 0,
registry_webhook_created_by INTEGER NOT NULL,
registry_webhook_created BIGINT NOT NULL,
registry_webhook_updated BIGINT NOT NULL,
registry_webhook_space_id INTEGER,
registry_webhook_registry_id INTEGER,
registry_webhook_name TEXT NOT NULL,
registry_webhook_description TEXT NOT NULL,
registry_webhook_url TEXT NOT NULL,
registry_webhook_secret_identifier TEXT,
registry_webhook_secret_space_id INTEGER,
registry_webhook_enabled BOOLEAN NOT NULL,
registry_webhook_insecure BOOLEAN NOT NULL,
registry_webhook_triggers TEXT NOT NULL,
registry_webhook_latest_execution_result TEXT,
registry_webhook_scope INTEGER DEFAULT 0,
registry_webhook_internal BOOLEAN NOT NULL,
registry_webhook_identifier TEXT NOT NULL,
registry_webhook_extra_headers TEXT,
CONSTRAINT fk_registry_webhook_created_by FOREIGN KEY (registry_webhook_created_by)
REFERENCES principals (principal_id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION,
CONSTRAINT fk_registry_webhook_registry_id FOREIGN KEY (registry_webhook_registry_id)
REFERENCES registries (registry_id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE CASCADE
);
CREATE UNIQUE INDEX registry_webhooks_registry_id_identifier
ON registry_webhooks(registry_webhook_registry_id, registry_webhook_identifier)
WHERE registry_webhook_space_id IS NULL;
CREATE UNIQUE INDEX registry_webhooks_space_id_identifier
ON registry_webhooks(registry_webhook_space_id, registry_webhook_identifier)
WHERE registry_webhook_registry_id IS NULL;
CREATE TABLE registry_webhook_executions
(
registry_webhook_execution_id SERIAL PRIMARY KEY,
registry_webhook_execution_retrigger_of INTEGER,
registry_webhook_execution_retriggerable BOOLEAN NOT NULL,
registry_webhook_execution_webhook_id INTEGER NOT NULL,
registry_webhook_execution_trigger_type TEXT NOT NULL,
registry_webhook_execution_trigger_id TEXT NOT NULL,
registry_webhook_execution_result TEXT NOT NULL,
registry_webhook_execution_created BIGINT NOT NULL,
registry_webhook_execution_duration BIGINT NOT NULL,
registry_webhook_execution_error TEXT NOT NULL,
registry_webhook_execution_request_url TEXT NOT NULL,
registry_webhook_execution_request_headers TEXT NOT NULL,
registry_webhook_execution_request_body TEXT NOT NULL,
registry_webhook_execution_response_status_code INTEGER NOT NULL,
registry_webhook_execution_response_status TEXT NOT NULL,
registry_webhook_execution_response_headers TEXT NOT NULL,
registry_webhook_execution_response_body TEXT NOT NULL
);

View File

@ -0,0 +1,4 @@
DROP INDEX registry_webhooks_registry_id_identifier;
DROP INDEX registry_webhooks_space_id_identifier;
DROP TABLE registry_webhook_executions;
DROP TABLE registry_webhooks;

View File

@ -0,0 +1,60 @@
CREATE TABLE registry_webhooks
(
registry_webhook_id INTEGER PRIMARY KEY AUTOINCREMENT,
registry_webhook_version INTEGER NOT NULL DEFAULT 0,
registry_webhook_created_by INTEGER NOT NULL,
registry_webhook_created BIGINT NOT NULL,
registry_webhook_updated BIGINT NOT NULL,
registry_webhook_space_id INTEGER,
registry_webhook_registry_id INTEGER,
registry_webhook_name TEXT NOT NULL,
registry_webhook_description TEXT NOT NULL,
registry_webhook_url TEXT NOT NULL,
registry_webhook_secret_identifier TEXT,
registry_webhook_secret_space_id INTEGER,
registry_webhook_enabled BOOLEAN NOT NULL,
registry_webhook_insecure BOOLEAN NOT NULL,
registry_webhook_triggers TEXT NOT NULL,
registry_webhook_latest_execution_result TEXT,
registry_webhook_scope INTEGER DEFAULT 0,
registry_webhook_internal BOOLEAN NOT NULL,
registry_webhook_identifier TEXT NOT NULL,
registry_webhook_extra_headers TEXT,
CONSTRAINT fk_registry_webhook_created_by FOREIGN KEY (registry_webhook_created_by)
REFERENCES principals (principal_id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION,
CONSTRAINT fk_registry_webhook_registry_id FOREIGN KEY (registry_webhook_registry_id)
REFERENCES registries (registry_id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE CASCADE
);
CREATE UNIQUE INDEX registry_webhooks_registry_id_identifier
ON registry_webhooks(registry_webhook_registry_id, registry_webhook_identifier)
WHERE registry_webhook_space_id IS NULL;
CREATE UNIQUE INDEX registry_webhooks_space_id_identifier
ON registry_webhooks(registry_webhook_space_id, registry_webhook_identifier)
WHERE registry_webhook_registry_id IS NULL;
CREATE TABLE registry_webhook_executions
(
registry_webhook_execution_id INTEGER PRIMARY KEY AUTOINCREMENT,
registry_webhook_execution_retrigger_of INTEGER,
registry_webhook_execution_retriggerable BOOLEAN NOT NULL,
registry_webhook_execution_webhook_id INTEGER NOT NULL,
registry_webhook_execution_trigger_type TEXT NOT NULL,
registry_webhook_execution_trigger_id TEXT NOT NULL,
registry_webhook_execution_result TEXT NOT NULL,
registry_webhook_execution_created BIGINT NOT NULL,
registry_webhook_execution_duration BIGINT NOT NULL,
registry_webhook_execution_error TEXT NOT NULL,
registry_webhook_execution_request_url TEXT NOT NULL,
registry_webhook_execution_request_headers TEXT NOT NULL,
registry_webhook_execution_request_body TEXT NOT NULL,
registry_webhook_execution_response_status_code INTEGER NOT NULL,
registry_webhook_execution_response_status TEXT NOT NULL,
registry_webhook_execution_response_headers TEXT NOT NULL,
registry_webhook_execution_response_body TEXT NOT NULL
);

View File

@ -481,7 +481,8 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
handler := api2.NewHandlerProvider(dockerController, spaceStore, tokenStore, controller, authenticator, provider, authorizer, config)
registryOCIHandler := router.OCIHandlerProvider(handler)
cleanupPolicyRepository := database2.ProvideCleanupPolicyDao(db, transactor)
apiHandler := router.APIHandlerProvider(registryRepository, upstreamProxyConfigRepository, tagRepository, manifestRepository, cleanupPolicyRepository, imageRepository, storageDriver, spaceStore, transactor, authenticator, provider, authorizer, auditService, spacePathStore, artifactRepository)
webhooksRepository := database2.ProvideWebhookDao(db)
apiHandler := router.APIHandlerProvider(registryRepository, upstreamProxyConfigRepository, tagRepository, manifestRepository, cleanupPolicyRepository, imageRepository, storageDriver, spaceStore, transactor, authenticator, provider, authorizer, auditService, spacePathStore, artifactRepository, webhooksRepository)
nodesRepository := database2.ProvideNodeDao(db)
mavenDBStore := maven.DBStoreProvider(registryRepository, imageRepository, artifactRepository, spaceStore, bandwidthStatRepository, downloadStatRepository, nodesRepository, upstreamProxyConfigRepository)
filemanagerApp := filemanager.NewApp(ctx, config, storageService)

View File

@ -28,6 +28,7 @@ import (
"github.com/harness/gitness/registry/app/pkg/commons"
"github.com/harness/gitness/registry/app/storage"
"github.com/harness/gitness/registry/types"
registryenum "github.com/harness/gitness/registry/types/enum"
digest "github.com/opencontainers/go-digest"
"github.com/rs/zerolog/log"
@ -47,6 +48,8 @@ type RegistryRequestBaseInfo struct {
ParentRef string
parentID int64
RegistryType api.RegistryType
}
type RegistryRequestInfo struct {
@ -129,6 +132,7 @@ func (c *APIController) GetRegistryRequestBaseInfo(
baseInfo.RegistryRef = regRef
baseInfo.RegistryIdentifier = regIdentifier
baseInfo.RegistryID = reg.ID
baseInfo.RegistryType = reg.Type
}
return baseInfo, nil
@ -404,3 +408,105 @@ func CreateUpstreamProxyResponseJSONResponse(upstreamproxy *types.UpstreamProxy)
}
return response
}
func (c *APIController) mapToWebhookResponseEntity(
ctx context.Context,
createdWebhook types.Webhook,
) (*api.Webhook, error) {
createdAt := GetTimeInMs(createdWebhook.CreatedAt)
modifiedAt := GetTimeInMs(createdWebhook.UpdatedAt)
webhookResponseEntity := &api.Webhook{
Identifier: createdWebhook.Identifier,
Name: createdWebhook.Name,
Description: &createdWebhook.Description,
Url: createdWebhook.URL,
Version: &createdWebhook.Version,
Enabled: createdWebhook.Enabled,
Internal: &createdWebhook.Internal,
Insecure: createdWebhook.Insecure,
Triggers: &createdWebhook.Triggers,
CreatedBy: &createdWebhook.CreatedBy,
CreatedAt: &createdAt,
ModifiedAt: &modifiedAt,
LatestExecutionResult: createdWebhook.LatestExecutionResult,
}
if createdWebhook.ExtraHeaders != nil {
webhookResponseEntity.ExtraHeaders = &createdWebhook.ExtraHeaders
}
secretSpacePath := ""
if createdWebhook.SecretSpaceID > 0 {
primary, err := c.spacePathStore.FindPrimaryBySpaceID(ctx, int64(createdWebhook.SecretSpaceID))
if err != nil {
return nil, fmt.Errorf("failed to get secret space path: %w", err)
}
secretSpacePath = primary.Value
}
if createdWebhook.SecretIdentifier != "" {
webhookResponseEntity.SecretIdentifier = &createdWebhook.SecretIdentifier
}
if secretSpacePath != "" {
webhookResponseEntity.SecretSpacePath = &secretSpacePath
}
if createdWebhook.SecretSpaceID > 0 {
webhookResponseEntity.SecretSpaceId = &createdWebhook.SecretSpaceID
}
return webhookResponseEntity, nil
}
func (c *APIController) mapToWebhook(
ctx context.Context,
webhookRequest api.WebhookRequest,
regInfo *RegistryRequestBaseInfo,
) (*types.Webhook, error) {
webhook := &types.Webhook{
Name: webhookRequest.Identifier,
ParentType: registryenum.WebhookParentRegistry,
ParentID: regInfo.RegistryID,
Scope: webhookScopeRegistry,
Identifier: webhookRequest.Identifier,
URL: webhookRequest.Url,
Enabled: webhookRequest.Enabled,
Insecure: webhookRequest.Insecure,
Triggers: deduplicateTriggers(*webhookRequest.Triggers),
}
if webhookRequest.Description != nil {
webhook.Description = *webhookRequest.Description
}
if webhookRequest.SecretIdentifier != nil {
webhook.SecretIdentifier = *webhookRequest.SecretIdentifier
}
if webhookRequest.ExtraHeaders != nil {
webhook.ExtraHeaders = *webhookRequest.ExtraHeaders
}
if webhookRequest.SecretSpacePath != nil && len(*webhookRequest.SecretSpacePath) > 0 {
secretSpaceID, err := c.getSecretSpaceID(ctx, webhookRequest.SecretSpacePath)
if err != nil {
return nil, err
}
webhook.SecretSpaceID = secretSpaceID
} else if webhookRequest.SecretSpaceId != nil {
webhook.SecretSpaceID = *webhookRequest.SecretSpaceId
}
return webhook, nil
}
// deduplicateTriggers de-duplicates the triggers provided by the user.
func deduplicateTriggers(in []api.Trigger) []api.Trigger {
if len(in) == 0 {
return []api.Trigger{}
}
triggerSet := make(map[api.Trigger]bool, len(in))
out := make([]api.Trigger, 0, len(in))
for _, trigger := range in {
if triggerSet[trigger] {
continue
}
triggerSet[trigger] = true
out = append(out, trigger)
}
return out
}

View File

@ -40,6 +40,7 @@ type APIController struct {
AuditService audit.Service
spacePathStore corestore.SpacePathStore
ArtifactStore store.ArtifactRepository
WebhooksRepository store.WebhooksRepository
}
func NewAPIController(
@ -57,6 +58,7 @@ func NewAPIController(
auditService audit.Service,
spacePathStore corestore.SpacePathStore,
artifactStore store.ArtifactRepository,
webhooksRepository store.WebhooksRepository,
) *APIController {
return &APIController{
RegistryRepository: repositoryStore,
@ -73,5 +75,6 @@ func NewAPIController(
AuditService: auditService,
spacePathStore: spacePathStore,
ArtifactStore: artifactStore,
WebhooksRepository: webhooksRepository,
}
}

View File

@ -329,7 +329,7 @@ func (c *APIController) CreateUpstreamProxyEntity(
}
if res.SecretSpacePath != nil && len(*res.SecretSpacePath) > 0 {
upstreamProxyConfigEntity.SecretSpaceID, err = c.getSecretID(ctx, res.SecretSpacePath)
upstreamProxyConfigEntity.SecretSpaceID, err = c.getSecretSpaceID(ctx, res.SecretSpacePath)
if err != nil {
return nil, nil, err
}
@ -350,7 +350,7 @@ func (c *APIController) CreateUpstreamProxyEntity(
return nil, nil, fmt.Errorf("failed to create upstream proxy: access_key_secret_identifier missing")
default:
if res.AccessKeySecretSpacePath != nil && len(*res.AccessKeySecretSpacePath) > 0 {
upstreamProxyConfigEntity.UserNameSecretSpaceID, err = c.getSecretID(ctx, res.AccessKeySecretSpacePath)
upstreamProxyConfigEntity.UserNameSecretSpaceID, err = c.getSecretSpaceID(ctx, res.AccessKeySecretSpacePath)
if err != nil {
return nil, nil, err
}
@ -361,7 +361,7 @@ func (c *APIController) CreateUpstreamProxyEntity(
}
if res.SecretKeySpacePath != nil && len(*res.SecretKeySpacePath) > 0 {
upstreamProxyConfigEntity.SecretSpaceID, err = c.getSecretID(ctx, res.SecretKeySpacePath)
upstreamProxyConfigEntity.SecretSpaceID, err = c.getSecretSpaceID(ctx, res.SecretKeySpacePath)
if err != nil {
return nil, nil, err
}
@ -373,9 +373,9 @@ func (c *APIController) CreateUpstreamProxyEntity(
return repoEntity, upstreamProxyConfigEntity, nil
}
func (c *APIController) getSecretID(ctx context.Context, secretSpacePath *string) (int, error) {
func (c *APIController) getSecretSpaceID(ctx context.Context, secretSpacePath *string) (int, error) {
if secretSpacePath == nil {
return -1, fmt.Errorf("failed to create upstream proxy: secret space missing")
return -1, fmt.Errorf("secret space path is missing")
}
path, err := c.spacePathStore.FindByPath(ctx, *secretSpacePath)

View File

@ -0,0 +1,124 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metadata
import (
"context"
"fmt"
"net/http"
apiauth "github.com/harness/gitness/app/api/auth"
"github.com/harness/gitness/app/api/request"
api "github.com/harness/gitness/registry/app/api/openapi/contracts/artifact"
"github.com/harness/gitness/types/enum"
"github.com/rs/zerolog/log"
)
const webhookScopeRegistry = int64(0)
func (c *APIController) CreateWebhook(
ctx context.Context,
r api.CreateWebhookRequestObject,
) (api.CreateWebhookResponseObject, error) {
webhookRequest := api.WebhookRequest(*r.Body)
regInfo, err := c.GetRegistryRequestBaseInfo(ctx, "", string(r.RegistryRef))
if err != nil {
return createWebhookBadRequestErrorResponse(err)
}
if regInfo.RegistryType != api.RegistryTypeVIRTUAL {
log.Ctx(ctx).Error().Msgf("failed to store webhook: %s with error: %v", webhookRequest.Identifier, err)
return createWebhookBadRequestErrorResponse(
fmt.Errorf("not allowed to create webhook for %s registry", regInfo.RegistryType),
)
}
space, err := c.SpaceStore.FindByRef(ctx, regInfo.ParentRef)
if err != nil {
return createWebhookBadRequestErrorResponse(err)
}
session, _ := request.AuthSessionFrom(ctx)
permissionChecks := GetPermissionChecks(space, regInfo.RegistryIdentifier, enum.PermissionRegistryEdit)
if err = apiauth.CheckRegistry(
ctx,
c.Authorizer,
session,
permissionChecks...,
); err != nil {
log.Ctx(ctx).Error().Msgf("permission check failed while creating webhook for registry: %s, error: %v",
regInfo.RegistryIdentifier, err)
return api.CreateWebhook403JSONResponse{
UnauthorizedJSONResponse: api.UnauthorizedJSONResponse(
*GetErrorResponse(http.StatusForbidden, err.Error()),
),
}, err
}
webhook, err := c.mapToWebhook(ctx, webhookRequest, regInfo)
webhook.Internal = false
webhook.CreatedBy = session.Principal.ID
if err != nil {
log.Ctx(ctx).Error().Msgf("failed to store webhook: %s with error: %v", webhookRequest.Identifier, err)
return createWebhookBadRequestErrorResponse(fmt.Errorf("failed to store webhook %w", err))
}
err = c.WebhooksRepository.Create(ctx, webhook)
if err != nil {
log.Ctx(ctx).Error().Msgf("failed to store webhook: %s with error: %v", webhookRequest.Identifier, err)
if isDuplicateKeyError(err) {
return createWebhookBadRequestErrorResponse(fmt.Errorf(
"failed to store webhook, Webhook with identifier %s already exists", webhookRequest.Identifier,
))
}
return createWebhookBadRequestErrorResponse(fmt.Errorf("failed to store webhook: %w", err))
}
createdWebhook, err := c.WebhooksRepository.GetByRegistryAndIdentifier(
ctx, regInfo.RegistryID, webhookRequest.Identifier,
)
if err != nil {
log.Ctx(ctx).Error().Msgf("failed to stored webhook: %s with error: %v",
webhookRequest.Identifier, err)
return createWebhookInternalErrorResponse(fmt.Errorf("failed to stored webhook: %w", err))
}
webhookResponseEntity, err := c.mapToWebhookResponseEntity(ctx, *createdWebhook)
if err != nil {
log.Ctx(ctx).Error().Msgf("failed to stored webhook: %s with error: %v",
webhookRequest.Identifier, err)
return createWebhookInternalErrorResponse(fmt.Errorf("failed to stored webhook: %w", err))
}
return api.CreateWebhook201JSONResponse{
WebhookResponseJSONResponse: api.WebhookResponseJSONResponse{
Data: *webhookResponseEntity,
Status: api.StatusSUCCESS,
},
}, nil
}
func createWebhookBadRequestErrorResponse(err error) (api.CreateWebhookResponseObject, error) {
return api.CreateWebhook400JSONResponse{
BadRequestJSONResponse: api.BadRequestJSONResponse(
*GetErrorResponse(http.StatusBadRequest, err.Error()),
),
}, err
}
func createWebhookInternalErrorResponse(err error) (api.CreateWebhookResponseObject, error) {
return api.CreateWebhook500JSONResponse{
InternalServerErrorJSONResponse: api.InternalServerErrorJSONResponse(
*GetErrorResponse(http.StatusInternalServerError, err.Error()),
),
}, err
}

View File

@ -0,0 +1,72 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metadata
import (
"context"
"net/http"
apiauth "github.com/harness/gitness/app/api/auth"
"github.com/harness/gitness/app/api/request"
api "github.com/harness/gitness/registry/app/api/openapi/contracts/artifact"
"github.com/harness/gitness/types/enum"
)
func (c *APIController) DeleteWebhook(
ctx context.Context,
r api.DeleteWebhookRequestObject,
) (api.DeleteWebhookResponseObject, error) {
regInfo, err := c.GetRegistryRequestBaseInfo(ctx, "", string(r.RegistryRef))
if err != nil {
return deleteWebhookInternalErrorResponse(err)
}
space, err := c.SpaceStore.FindByRef(ctx, regInfo.ParentRef)
if err != nil {
return deleteWebhookInternalErrorResponse(err)
}
session, _ := request.AuthSessionFrom(ctx)
permissionChecks := GetPermissionChecks(space, regInfo.RegistryIdentifier, enum.PermissionRegistryEdit)
if err = apiauth.CheckRegistry(
ctx,
c.Authorizer,
session,
permissionChecks...,
); err != nil {
return api.DeleteWebhook403JSONResponse{
UnauthorizedJSONResponse: api.UnauthorizedJSONResponse(
*GetErrorResponse(http.StatusForbidden, err.Error()),
),
}, err
}
webhookIdentifier := string(r.WebhookIdentifier)
err = c.WebhooksRepository.DeleteByRegistryAndIdentifier(ctx, regInfo.RegistryID, webhookIdentifier)
if err != nil {
return deleteWebhookInternalErrorResponse(err)
}
return api.DeleteWebhook200JSONResponse{
SuccessJSONResponse: api.SuccessJSONResponse(*GetSuccessResponse()),
}, nil
}
func deleteWebhookInternalErrorResponse(err error) (api.DeleteWebhookResponseObject, error) {
return api.DeleteWebhook500JSONResponse{
InternalServerErrorJSONResponse: api.InternalServerErrorJSONResponse(
*GetErrorResponse(http.StatusInternalServerError, err.Error()),
),
}, err
}

View File

@ -0,0 +1,89 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metadata
import (
"context"
"fmt"
"net/http"
apiauth "github.com/harness/gitness/app/api/auth"
"github.com/harness/gitness/app/api/request"
api "github.com/harness/gitness/registry/app/api/openapi/contracts/artifact"
"github.com/harness/gitness/types/enum"
"github.com/rs/zerolog/log"
)
func (c *APIController) GetWebhook(
ctx context.Context,
r api.GetWebhookRequestObject,
) (api.GetWebhookResponseObject, error) {
regInfo, err := c.GetRegistryRequestBaseInfo(ctx, "", string(r.RegistryRef))
if err != nil {
log.Ctx(ctx).Error().Msgf("failed to get registry details: %v", err)
return getWebhookInternalErrorResponse(err)
}
space, err := c.SpaceStore.FindByRef(ctx, regInfo.ParentRef)
if err != nil {
log.Ctx(ctx).Error().Msgf("failed to find space: %v", err)
return getWebhookInternalErrorResponse(err)
}
session, _ := request.AuthSessionFrom(ctx)
permissionChecks := GetPermissionChecks(space, regInfo.RegistryIdentifier, enum.PermissionRegistryView)
if err = apiauth.CheckRegistry(
ctx,
c.Authorizer,
session,
permissionChecks...,
); err != nil {
log.Ctx(ctx).Error().Msgf("permission check failed while getting webhook for registry: %s, error: %v",
regInfo.RegistryIdentifier, err)
return api.GetWebhook403JSONResponse{
UnauthorizedJSONResponse: api.UnauthorizedJSONResponse(
*GetErrorResponse(http.StatusForbidden, err.Error()),
),
}, err
}
webhookIdentifier := string(r.WebhookIdentifier)
webhook, err := c.WebhooksRepository.GetByRegistryAndIdentifier(ctx, regInfo.RegistryID, webhookIdentifier)
if err != nil {
log.Ctx(ctx).Error().Msgf("failed to get webhook: %s with error: %v", webhookIdentifier, err)
return getWebhookInternalErrorResponse(fmt.Errorf("failed to get webhook"))
}
webhookResponseEntity, err := c.mapToWebhookResponseEntity(ctx, *webhook)
if err != nil {
log.Ctx(ctx).Error().Msgf("failed to get webhook: %s with error: %v", webhookIdentifier, err)
return getWebhookInternalErrorResponse(fmt.Errorf("failed to get webhook"))
}
return api.GetWebhook200JSONResponse{
WebhookResponseJSONResponse: api.WebhookResponseJSONResponse{
Data: *webhookResponseEntity,
Status: api.StatusSUCCESS,
},
}, nil
}
func getWebhookInternalErrorResponse(err error) (api.GetWebhookResponseObject, error) {
return api.GetWebhook500JSONResponse{
InternalServerErrorJSONResponse: api.InternalServerErrorJSONResponse(
*GetErrorResponse(http.StatusInternalServerError, err.Error()),
),
}, err
}

View File

@ -0,0 +1,33 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metadata
import (
"context"
api "github.com/harness/gitness/registry/app/api/openapi/contracts/artifact"
)
func (c *APIController) GetWebhookExecution(
_ context.Context,
_ api.GetWebhookExecutionRequestObject,
) (api.GetWebhookExecutionResponseObject, error) {
return api.GetWebhookExecution200JSONResponse{
WebhookExecutionResponseJSONResponse: api.WebhookExecutionResponseJSONResponse{
Data: api.WebhookExecution{},
Status: api.StatusSUCCESS,
},
}, nil
}

View File

@ -0,0 +1,33 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metadata
import (
"context"
api "github.com/harness/gitness/registry/app/api/openapi/contracts/artifact"
)
func (c *APIController) ListWebhookExecutions(
_ context.Context,
_ api.ListWebhookExecutionsRequestObject,
) (api.ListWebhookExecutionsResponseObject, error) {
return api.ListWebhookExecutions200JSONResponse{
ListWebhooksExecutionResponseJSONResponse: api.ListWebhooksExecutionResponseJSONResponse{
Data: api.ListWebhooksExecutions{},
Status: api.StatusSUCCESS,
},
}, nil
}

View File

@ -0,0 +1,140 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metadata
import (
"context"
"fmt"
"net/http"
apiauth "github.com/harness/gitness/app/api/auth"
"github.com/harness/gitness/app/api/request"
api "github.com/harness/gitness/registry/app/api/openapi/contracts/artifact"
"github.com/harness/gitness/registry/types"
"github.com/harness/gitness/types/enum"
"github.com/rs/zerolog/log"
)
func (c *APIController) ListWebhooks(
ctx context.Context,
r api.ListWebhooksRequestObject,
) (api.ListWebhooksResponseObject, error) {
regInfo, err := c.GetRegistryRequestBaseInfo(ctx, "", string(r.RegistryRef))
if err != nil {
return listWebhookInternalErrorResponse(err)
}
space, err := c.SpaceStore.FindByRef(ctx, regInfo.ParentRef)
if err != nil {
return listWebhookInternalErrorResponse(err)
}
session, _ := request.AuthSessionFrom(ctx)
permissionChecks := GetPermissionChecks(space, regInfo.RegistryIdentifier, enum.PermissionRegistryView)
if err = apiauth.CheckRegistry(
ctx,
c.Authorizer,
session,
permissionChecks...,
); err != nil {
log.Ctx(ctx).Error().Msgf("permission check failed while listing webhook for registry: %s, error: %v",
regInfo.RegistryIdentifier, err)
return api.ListWebhooks403JSONResponse{
UnauthorizedJSONResponse: api.UnauthorizedJSONResponse(
*GetErrorResponse(http.StatusForbidden, err.Error()),
),
}, err
}
offset := GetOffset(r.Params.Size, r.Params.Page)
limit := GetPageLimit(r.Params.Size)
pageNumber := GetPageNumber(r.Params.Page)
searchTerm := ""
if r.Params.SearchTerm != nil {
searchTerm = string(*r.Params.SearchTerm)
}
sortByField := ""
sortByOrder := ""
if r.Params.SortOrder != nil {
sortByOrder = string(*r.Params.SortOrder)
}
if r.Params.SortField != nil {
sortByField = string(*r.Params.SortField)
}
webhooks, err := c.WebhooksRepository.ListByRegistry(
ctx,
sortByField,
sortByOrder,
limit,
offset,
searchTerm,
regInfo.RegistryID,
)
if err != nil {
log.Ctx(ctx).Error().Msgf("failed to list webhooks for registry: %s with error: %v", regInfo.RegistryRef, err)
return listWebhookInternalErrorResponse(fmt.Errorf("failed list to webhooks"))
}
count, err := c.WebhooksRepository.CountAllByRegistry(ctx, regInfo.RegistryID, searchTerm)
if err != nil {
log.Ctx(ctx).Error().Msgf("failed to list webhooks for registry: %s with error: %v", regInfo.RegistryRef, err)
return listWebhookInternalErrorResponse(fmt.Errorf("failed list to webhooks"))
}
webhooksResponse, err := c.mapToListWebhookResponseEntity(ctx, webhooks)
if err != nil {
log.Ctx(ctx).Error().Msgf("failed to list webhooks for registry: %s with error: %v", regInfo.RegistryRef, err)
return listWebhookInternalErrorResponse(fmt.Errorf("failed to list webhooks"))
}
pageCount := GetPageCount(count, limit)
return api.ListWebhooks200JSONResponse{
ListWebhooksResponseJSONResponse: api.ListWebhooksResponseJSONResponse{
Data: api.ListWebhooks{
PageIndex: &pageNumber,
PageCount: &pageCount,
PageSize: &limit,
ItemCount: &count,
Webhooks: webhooksResponse,
},
Status: api.StatusSUCCESS,
},
}, nil
}
func listWebhookInternalErrorResponse(err error) (api.ListWebhooksResponseObject, error) {
return api.ListWebhooks500JSONResponse{
InternalServerErrorJSONResponse: api.InternalServerErrorJSONResponse(
*GetErrorResponse(http.StatusInternalServerError, err.Error()),
),
}, err
}
func (c *APIController) mapToListWebhookResponseEntity(
ctx context.Context,
webhooks *[]types.Webhook,
) ([]api.Webhook, error) {
webhooksEntities := make([]api.Webhook, 0, len(*webhooks))
for _, d := range *webhooks {
webhook, err := c.mapToWebhookResponseEntity(ctx, d)
if err != nil {
return nil, err
}
webhooksEntities = append(webhooksEntities, *webhook)
}
return webhooksEntities, nil
}

View File

@ -0,0 +1,33 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metadata
import (
"context"
api "github.com/harness/gitness/registry/app/api/openapi/contracts/artifact"
)
func (c *APIController) ReTriggerWebhookExecution(
_ context.Context,
_ api.ReTriggerWebhookExecutionRequestObject,
) (api.ReTriggerWebhookExecutionResponseObject, error) {
return api.ReTriggerWebhookExecution200JSONResponse{
WebhookExecutionResponseJSONResponse: api.WebhookExecutionResponseJSONResponse{
Data: api.WebhookExecution{},
Status: api.StatusSUCCESS,
},
}, nil
}

View File

@ -392,7 +392,7 @@ func (c *APIController) UpdateUpstreamProxyEntity(
}
if res.SecretSpacePath != nil && len(*res.SecretSpacePath) > 0 {
upstreamProxyConfigEntity.SecretSpaceID, err = c.getSecretID(ctx, res.SecretSpacePath)
upstreamProxyConfigEntity.SecretSpaceID, err = c.getSecretSpaceID(ctx, res.SecretSpacePath)
if err != nil {
return nil, nil, err
}
@ -412,7 +412,7 @@ func (c *APIController) UpdateUpstreamProxyEntity(
return nil, nil, fmt.Errorf("failed to create upstream proxy: access_key_secret_identifier missing")
default:
if res.AccessKeySecretSpacePath != nil && len(*res.AccessKeySecretSpacePath) > 0 {
upstreamProxyConfigEntity.UserNameSecretSpaceID, err = c.getSecretID(ctx, res.AccessKeySecretSpacePath)
upstreamProxyConfigEntity.UserNameSecretSpaceID, err = c.getSecretSpaceID(ctx, res.AccessKeySecretSpacePath)
if err != nil {
return nil, nil, err
}
@ -423,7 +423,7 @@ func (c *APIController) UpdateUpstreamProxyEntity(
}
if res.SecretKeySpacePath != nil && len(*res.SecretKeySpacePath) > 0 {
upstreamProxyConfigEntity.SecretSpaceID, err = c.getSecretID(ctx, res.SecretKeySpacePath)
upstreamProxyConfigEntity.SecretSpaceID, err = c.getSecretSpaceID(ctx, res.SecretKeySpacePath)
if err != nil {
return nil, nil, err
}

View File

@ -0,0 +1,112 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metadata
import (
"context"
"fmt"
"net/http"
apiauth "github.com/harness/gitness/app/api/auth"
"github.com/harness/gitness/app/api/request"
api "github.com/harness/gitness/registry/app/api/openapi/contracts/artifact"
"github.com/harness/gitness/types/enum"
"github.com/rs/zerolog/log"
)
func (c *APIController) UpdateWebhook(
ctx context.Context,
r api.UpdateWebhookRequestObject,
) (api.UpdateWebhookResponseObject, error) {
webhookRequest := api.WebhookRequest(*r.Body)
regInfo, err := c.GetRegistryRequestBaseInfo(ctx, "", string(r.RegistryRef))
if err != nil {
return updateWebhookInternalErrorResponse(err)
}
space, err := c.SpaceStore.FindByRef(ctx, regInfo.ParentRef)
if err != nil {
return updateWebhookInternalErrorResponse(err)
}
session, _ := request.AuthSessionFrom(ctx)
permissionChecks := GetPermissionChecks(space, regInfo.RegistryIdentifier, enum.PermissionRegistryEdit)
if err = apiauth.CheckRegistry(
ctx,
c.Authorizer,
session,
permissionChecks...,
); err != nil {
log.Ctx(ctx).Error().Msgf("permission check failed while updating webhook for registry: %s, error: %v",
regInfo.RegistryIdentifier, err)
return api.UpdateWebhook403JSONResponse{
UnauthorizedJSONResponse: api.UnauthorizedJSONResponse(
*GetErrorResponse(http.StatusForbidden, err.Error()),
),
}, err
}
webhook, err := c.mapToWebhook(ctx, webhookRequest, regInfo)
if err != nil {
log.Ctx(ctx).Error().Msgf("failed to update webhook: %s with error: %v", webhookRequest.Identifier, err)
return updateWebhookBadRequestErrorResponse(fmt.Errorf("failed to update webhook"))
}
webhook.Identifier = string(r.WebhookIdentifier)
err = c.WebhooksRepository.Update(ctx, webhook)
if err != nil {
log.Ctx(ctx).Error().Msgf("failed to update webhook: %s for registry: %s with error: %v",
webhookRequest.Identifier, regInfo.RegistryRef, err)
return updateWebhookBadRequestErrorResponse(fmt.Errorf("failed to update webhook"))
}
updatedWebhook, err := c.WebhooksRepository.GetByRegistryAndIdentifier(
ctx, regInfo.RegistryID, webhookRequest.Identifier,
)
if err != nil {
log.Ctx(ctx).Error().Msgf("failed to get updated webhook: %s with error: %v",
webhookRequest.Identifier, err)
return updateWebhookInternalErrorResponse(fmt.Errorf("failed to get updated webhook"))
}
webhookResponseEntity, err := c.mapToWebhookResponseEntity(ctx, *updatedWebhook)
if err != nil {
log.Ctx(ctx).Error().Msgf("failed to get updated webhook: %s with error: %v",
webhookRequest.Identifier, err)
return updateWebhookInternalErrorResponse(fmt.Errorf("failed to get updated webhook"))
}
return api.UpdateWebhook201JSONResponse{
WebhookResponseJSONResponse: api.WebhookResponseJSONResponse{
Data: *webhookResponseEntity,
Status: api.StatusSUCCESS,
},
}, nil
}
func updateWebhookInternalErrorResponse(err error) (api.UpdateWebhookResponseObject, error) {
return api.UpdateWebhook500JSONResponse{
InternalServerErrorJSONResponse: api.InternalServerErrorJSONResponse(
*GetErrorResponse(http.StatusInternalServerError, err.Error()),
),
}, err
}
func updateWebhookBadRequestErrorResponse(err error) (api.UpdateWebhookResponseObject, error) {
return api.UpdateWebhook400JSONResponse{
BadRequestJSONResponse: api.BadRequestJSONResponse(
*GetErrorResponse(http.StatusBadRequest, err.Error()),
),
}, err
}

View File

@ -15,6 +15,8 @@ tags:
description: APIs to get details of docker artifacts
- name: Helm Artifacts
description: APIs to get details of helm artifacts
- name: Webhooks
description: APIs to create, update, list webhooks
servers:
@ -665,6 +667,184 @@ paths:
$ref: "#/components/responses/NotFound"
500:
$ref: "#/components/responses/InternalServerError"
/registry/{registry_ref}/webhooks:
post:
summary: CreateWebhook
description: Returns Webhook Details
operationId: CreateWebhook
tags:
- Webhooks
parameters:
- $ref: "#/components/parameters/registryRefPathParam"
requestBody:
$ref: "#/components/requestBodies/WebhookRequest"
responses:
201:
$ref: "#/components/responses/WebhookResponse"
400:
$ref: "#/components/responses/BadRequest"
401:
$ref: "#/components/responses/Unauthenticated"
403:
$ref: "#/components/responses/Unauthorized"
500:
$ref: "#/components/responses/InternalServerError"
get:
summary: ListWebhooks
description: Returns List of Webhook Details
operationId: ListWebhooks
tags:
- Webhooks
parameters:
- $ref: "#/components/parameters/registryRefPathParam"
- $ref: "#/components/parameters/pageNumber"
- $ref: "#/components/parameters/pageSize"
- $ref: "#/components/parameters/sortOrder"
- $ref: "#/components/parameters/sortField"
- $ref: "#/components/parameters/searchTerm"
responses:
200:
$ref: "#/components/responses/ListWebhooksResponse"
400:
$ref: "#/components/responses/BadRequest"
401:
$ref: "#/components/responses/Unauthenticated"
403:
$ref: "#/components/responses/Unauthorized"
500:
$ref: "#/components/responses/InternalServerError"
/registry/{registry_ref}/webhooks/{webhook_identifier}:
put:
summary: UpdateWebhook
description: Returns Webhook Details
operationId: UpdateWebhook
tags:
- Webhooks
parameters:
- $ref: "#/components/parameters/registryRefPathParam"
- $ref: "#/components/parameters/webhookIdentifierPathParam"
requestBody:
$ref: "#/components/requestBodies/WebhookRequest"
responses:
201:
$ref: "#/components/responses/WebhookResponse"
400:
$ref: "#/components/responses/BadRequest"
401:
$ref: "#/components/responses/Unauthenticated"
403:
$ref: "#/components/responses/Unauthorized"
500:
$ref: "#/components/responses/InternalServerError"
get:
summary: GetWebhook
description: Returns Webhook Details
operationId: GetWebhook
tags:
- Webhooks
parameters:
- $ref: "#/components/parameters/registryRefPathParam"
- $ref: "#/components/parameters/webhookIdentifierPathParam"
responses:
200:
$ref: "#/components/responses/WebhookResponse"
400:
$ref: "#/components/responses/BadRequest"
401:
$ref: "#/components/responses/Unauthenticated"
403:
$ref: "#/components/responses/Unauthorized"
500:
$ref: "#/components/responses/InternalServerError"
delete:
summary: DeleteWebhook
description: Delete a Webhook
operationId: DeleteWebhook
tags:
- Webhooks
parameters:
- $ref: "#/components/parameters/registryRefPathParam"
- $ref: "#/components/parameters/webhookIdentifierPathParam"
responses:
200:
$ref: "#/components/responses/Success"
400:
$ref: "#/components/responses/BadRequest"
401:
$ref: "#/components/responses/Unauthenticated"
403:
$ref: "#/components/responses/Unauthorized"
404:
$ref: "#/components/responses/NotFound"
500:
$ref: "#/components/responses/InternalServerError"
/registry/{registry_ref}/webhooks/{webhook_identifier}/executions:
get:
summary: ListWebhookExecutions
description: Returns Webhook Execution Details List
operationId: ListWebhookExecutions
tags:
- Webhooks
parameters:
- $ref: "#/components/parameters/registryRefPathParam"
- $ref: "#/components/parameters/webhookIdentifierPathParam"
- $ref: "#/components/parameters/pageNumber"
- $ref: "#/components/parameters/pageSize"
responses:
200:
$ref: "#/components/responses/ListWebhooksExecutionResponse"
400:
$ref: "#/components/responses/BadRequest"
401:
$ref: "#/components/responses/Unauthenticated"
403:
$ref: "#/components/responses/Unauthorized"
500:
$ref: "#/components/responses/InternalServerError"
/registry/{registry_ref}/webhooks/{webhook_identifier}/executions/{webhook_execution_id}:
get:
summary: GetWebhookExecution
description: Returns Webhook Execution Details
operationId: GetWebhookExecution
tags:
- Webhooks
parameters:
- $ref: "#/components/parameters/registryRefPathParam"
- $ref: "#/components/parameters/webhookIdentifierPathParam"
- $ref: "#/components/parameters/webhookExecutionIdPathParam"
responses:
200:
$ref: "#/components/responses/WebhookExecutionResponse"
400:
$ref: "#/components/responses/BadRequest"
401:
$ref: "#/components/responses/Unauthenticated"
403:
$ref: "#/components/responses/Unauthorized"
500:
$ref: "#/components/responses/InternalServerError"
/registry/{registry_ref}/webhooks/{webhook_identifier}/executions/{webhook_execution_id}/retrigger:
get:
summary: ReTriggerWebhookExecution
description: Retrigger Webhook Execution
operationId: ReTriggerWebhookExecution
tags:
- Webhooks
parameters:
- $ref: "#/components/parameters/registryRefPathParam"
- $ref: "#/components/parameters/webhookIdentifierPathParam"
- $ref: "#/components/parameters/webhookExecutionIdPathParam"
responses:
200:
$ref: "#/components/responses/WebhookExecutionResponse"
400:
$ref: "#/components/responses/BadRequest"
401:
$ref: "#/components/responses/Unauthenticated"
403:
$ref: "#/components/responses/Unauthorized"
500:
$ref: "#/components/responses/InternalServerError"
components:
requestBodies:
RegistryRequest:
@ -679,6 +859,12 @@ components:
application/json:
schema:
$ref: "#/components/schemas/ArtifactLabelRequest"
WebhookRequest:
description: request for create and update webhook
content:
application/json:
schema:
$ref: "#/components/schemas/WebhookRequest"
responses:
ArtifactStatsResponse:
description: response to get artifact stats response
@ -736,6 +922,48 @@ components:
required:
- status
- data
WebhookResponse:
description: response for create, get and update webhook
content:
application/json:
schema:
type: object
properties:
status:
$ref: "#/components/schemas/Status"
data:
$ref: "#/components/schemas/Webhook"
required:
- status
- data
ListWebhooksExecutionResponse:
description: list webhooks executions response
content:
application/json:
schema:
type: object
properties:
status:
$ref: "#/components/schemas/Status"
data:
$ref: "#/components/schemas/ListWebhooksExecutions"
required:
- status
- data
WebhookExecutionResponse:
description: webhook execution response
content:
application/json:
schema:
type: object
properties:
status:
$ref: "#/components/schemas/Status"
data:
$ref: "#/components/schemas/WebhookExecution"
required:
- status
- data
DockerArtifactDetailResponse:
description: response to get docker artifact detail
content:
@ -927,6 +1155,20 @@ components:
required:
- status
- data
ListWebhooksResponse:
description: response for list webhooks
content:
application/json:
schema:
type: object
properties:
status:
$ref: "#/components/schemas/Status"
data:
$ref: "#/components/schemas/ListWebhooks"
required:
- status
- data
ListArtifactResponse:
description: response for list artifact
content:
@ -1057,6 +1299,66 @@ components:
$ref: "#/components/schemas/RegistryMetadata"
required:
- registries
ListWebhooks:
type: object
description: A list of Harness Registries webhooks
properties:
pageCount:
type: integer
format: int64
description: The total number of pages
example: 100
itemCount:
type: integer
format: int64
description: The total number of items
example: 1
pageSize:
type: integer
description: The number of items per page
example: 1
pageIndex:
type: integer
format: int64
description: The current page
example: 0
webhooks:
type: array
description: A list of Registries webhooks
items:
$ref: "#/components/schemas/Webhook"
required:
- webhooks
ListWebhooksExecutions:
type: object
description: A list of Harness Registries webhooks executions
properties:
pageCount:
type: integer
format: int64
description: The total number of pages
example: 100
itemCount:
type: integer
format: int64
description: The total number of items
example: 1
pageSize:
type: integer
description: The number of items per page
example: 1
pageIndex:
type: integer
format: int64
description: The current page
example: 0
executions:
type: array
description: A list of Registries webhooks executions
items:
$ref: "#/components/schemas/WebhookExecution"
required:
- executions
ListArtifact:
type: object
description: A list of Artifacts
@ -1445,6 +1747,109 @@ components:
properties:
pullCommand:
type: string
Webhook:
type: object
description: Harness Regstries Webhook
properties:
version:
type: integer
format: int64
identifier:
type: string
name:
type: string
description:
type: string
url:
type: string
createdAt:
type: string
createdBy:
type: integer
format: int64
modifiedAt:
type: string
enabled:
type: boolean
internal:
type: boolean
secretIdentifier:
type: string
secretSpacePath:
type: string
secretSpaceId:
type: integer
insecure:
type: boolean
triggers:
type: array
items:
$ref: "#/components/schemas/Trigger"
latestExecutionResult:
$ref: "#/components/schemas/WebhookExecResult"
extraHeaders:
type: array
items:
$ref: "#/components/schemas/ExtraHeader"
required:
- identifier
- url
- name
- enabled
- insecure
WebhookExecution:
type: object
description: Harness Regstries Webhook Execution
properties:
id:
type: integer
format: int64
retriggerOf:
type: integer
format: int64
retriggerable:
type: boolean
created:
type: integer
format: int64
webhookId:
type: integer
format: int64
triggerType:
$ref: "#/components/schemas/Trigger"
result:
$ref: "#/components/schemas/WebhookExecResult"
duration:
type: integer
format: int64
error:
type: string
request:
$ref: "#/components/schemas/WebhookExecRequest"
response:
$ref: "#/components/schemas/WebhookExecResponse"
WebhookExecRequest:
type: object
description: Harness Regstries HTTP Webhook Request
properties:
url:
type: string
headers:
type: string
body:
type: string
WebhookExecResponse:
type: object
description: Harness Regstries HTTP Webhook Response
properties:
statusCode:
type: integer
status:
type: string
headers:
type: string
body:
type: string
DockerArtifactDetail:
type: object
description: Docker Artifact Detail
@ -1710,6 +2115,28 @@ components:
type: array
items:
type: string
Trigger:
type: string
description: refers to trigger
enum:
- ARTIFACT_CREATION
- ARTIFACT_MODIFICATION
- ARTIFACT_DELETION
ExtraHeader:
type: object
description: Webhook Extra Header
properties:
key:
type: string
value:
type: string
WebhookExecResult:
type: string
description: refers to webhook execution
enum:
- SUCCESS
- RETRIABLE_ERROR
- FATAL_ERROR
RegistryType:
type: string
description: refers to type of registry i.e virtual or upstream
@ -1803,6 +2230,41 @@ components:
- identifier
- type
- packageType
WebhookRequest:
type: object
properties:
identifier:
type: string
name:
type: string
description:
type: string
url:
type: string
enabled:
type: boolean
insecure:
type: boolean
secretIdentifier:
type: string
secretSpacePath:
type: string
secretSpaceId:
type: integer
triggers:
type: array
items:
$ref: "#/components/schemas/Trigger"
extraHeaders:
type: array
items:
$ref: "#/components/schemas/ExtraHeader"
required:
- insecure
- enabled
- identifier
- url
- name
ArtifactLabelRequest:
type: object
properties:
@ -1910,6 +2372,20 @@ components:
description: Unique registry path.
schema:
type: string
webhookIdentifierPathParam:
name: webhook_identifier
in: path
required: true
description: Unique webhook identifier.
schema:
type: string
webhookExecutionIdPathParam:
name: webhook_execution_id
in: path
required: true
description: Unique webhook execution identifier.
schema:
type: string
artifactParam:
name: artifact
in: query

File diff suppressed because it is too large Load Diff

View File

@ -45,6 +45,13 @@ const (
StatusSUCCESS Status = "SUCCESS"
)
// Defines values for Trigger.
const (
TriggerARTIFACTCREATION Trigger = "ARTIFACT_CREATION"
TriggerARTIFACTDELETION Trigger = "ARTIFACT_DELETION"
TriggerARTIFACTMODIFICATION Trigger = "ARTIFACT_MODIFICATION"
)
// Defines values for UpstreamConfigSource.
const (
UpstreamConfigSourceAwsEcr UpstreamConfigSource = "AwsEcr"
@ -53,6 +60,13 @@ const (
UpstreamConfigSourceMavenCentral UpstreamConfigSource = "MavenCentral"
)
// Defines values for WebhookExecResult.
const (
WebhookExecResultFATALERROR WebhookExecResult = "FATAL_ERROR"
WebhookExecResultRETRIABLEERROR WebhookExecResult = "RETRIABLE_ERROR"
WebhookExecResultSUCCESS WebhookExecResult = "SUCCESS"
)
// Defines values for RegistryTypeParam.
const (
UPSTREAM RegistryTypeParam = "UPSTREAM"
@ -272,6 +286,12 @@ type Error struct {
Message string `json:"message"`
}
// ExtraHeader Webhook Extra Header
type ExtraHeader struct {
Key *string `json:"key,omitempty"`
Value *string `json:"value,omitempty"`
}
// FileDetail File Detail
type FileDetail struct {
Checksums []string `json:"checksums"`
@ -401,6 +421,42 @@ type ListRegistryArtifact struct {
PageSize *int `json:"pageSize,omitempty"`
}
// ListWebhooks A list of Harness Registries webhooks
type ListWebhooks struct {
// ItemCount The total number of items
ItemCount *int64 `json:"itemCount,omitempty"`
// PageCount The total number of pages
PageCount *int64 `json:"pageCount,omitempty"`
// PageIndex The current page
PageIndex *int64 `json:"pageIndex,omitempty"`
// PageSize The number of items per page
PageSize *int `json:"pageSize,omitempty"`
// Webhooks A list of Registries webhooks
Webhooks []Webhook `json:"webhooks"`
}
// ListWebhooksExecutions A list of Harness Registries webhooks executions
type ListWebhooksExecutions struct {
// Executions A list of Registries webhooks executions
Executions []WebhookExecution `json:"executions"`
// ItemCount The total number of items
ItemCount *int64 `json:"itemCount,omitempty"`
// PageCount The total number of pages
PageCount *int64 `json:"pageCount,omitempty"`
// PageIndex The current page
PageIndex *int64 `json:"pageIndex,omitempty"`
// PageSize The number of items per page
PageSize *int `json:"pageSize,omitempty"`
}
// MavenArtifactDetailConfig Config for generic artifact details
type MavenArtifactDetailConfig struct {
ArtifactId *string `json:"artifactId,omitempty"`
@ -492,6 +548,9 @@ type RegistryType string
// Status Indicates if the request was successful or not
type Status string
// Trigger refers to trigger
type Trigger string
// UpstreamConfig Configuration for Harness Artifact UpstreamProxies
type UpstreamConfig struct {
Auth *UpstreamConfig_Auth `json:"auth,omitempty"`
@ -523,6 +582,85 @@ type VirtualConfig struct {
UpstreamProxies *[]string `json:"upstreamProxies,omitempty"`
}
// Webhook Harness Regstries Webhook
type Webhook struct {
CreatedAt *string `json:"createdAt,omitempty"`
CreatedBy *int64 `json:"createdBy,omitempty"`
Description *string `json:"description,omitempty"`
Enabled bool `json:"enabled"`
ExtraHeaders *[]ExtraHeader `json:"extraHeaders,omitempty"`
Identifier string `json:"identifier"`
Insecure bool `json:"insecure"`
Internal *bool `json:"internal,omitempty"`
// LatestExecutionResult refers to webhook execution
LatestExecutionResult *WebhookExecResult `json:"latestExecutionResult,omitempty"`
ModifiedAt *string `json:"modifiedAt,omitempty"`
Name string `json:"name"`
SecretIdentifier *string `json:"secretIdentifier,omitempty"`
SecretSpaceId *int `json:"secretSpaceId,omitempty"`
SecretSpacePath *string `json:"secretSpacePath,omitempty"`
Triggers *[]Trigger `json:"triggers,omitempty"`
Url string `json:"url"`
Version *int64 `json:"version,omitempty"`
}
// WebhookExecRequest Harness Regstries HTTP Webhook Request
type WebhookExecRequest struct {
Body *string `json:"body,omitempty"`
Headers *string `json:"headers,omitempty"`
Url *string `json:"url,omitempty"`
}
// WebhookExecResponse Harness Regstries HTTP Webhook Response
type WebhookExecResponse struct {
Body *string `json:"body,omitempty"`
Headers *string `json:"headers,omitempty"`
Status *string `json:"status,omitempty"`
StatusCode *int `json:"statusCode,omitempty"`
}
// WebhookExecResult refers to webhook execution
type WebhookExecResult string
// WebhookExecution Harness Regstries Webhook Execution
type WebhookExecution struct {
Created *int64 `json:"created,omitempty"`
Duration *int64 `json:"duration,omitempty"`
Error *string `json:"error,omitempty"`
Id *int64 `json:"id,omitempty"`
// Request Harness Regstries HTTP Webhook Request
Request *WebhookExecRequest `json:"request,omitempty"`
// Response Harness Regstries HTTP Webhook Response
Response *WebhookExecResponse `json:"response,omitempty"`
// Result refers to webhook execution
Result *WebhookExecResult `json:"result,omitempty"`
RetriggerOf *int64 `json:"retriggerOf,omitempty"`
Retriggerable *bool `json:"retriggerable,omitempty"`
// TriggerType refers to trigger
TriggerType *Trigger `json:"triggerType,omitempty"`
WebhookId *int64 `json:"webhookId,omitempty"`
}
// WebhookRequest defines model for WebhookRequest.
type WebhookRequest struct {
Description *string `json:"description,omitempty"`
Enabled bool `json:"enabled"`
ExtraHeaders *[]ExtraHeader `json:"extraHeaders,omitempty"`
Identifier string `json:"identifier"`
Insecure bool `json:"insecure"`
Name string `json:"name"`
SecretIdentifier *string `json:"secretIdentifier,omitempty"`
SecretSpaceId *int `json:"secretSpaceId,omitempty"`
SecretSpacePath *string `json:"secretSpacePath,omitempty"`
Triggers *[]Trigger `json:"triggers,omitempty"`
Url string `json:"url"`
}
// LabelsParam defines model for LabelsParam.
type LabelsParam []string
@ -589,6 +727,12 @@ type VersionParam string
// VersionPathParam defines model for versionPathParam.
type VersionPathParam string
// WebhookExecutionIdPathParam defines model for webhookExecutionIdPathParam.
type WebhookExecutionIdPathParam string
// WebhookIdentifierPathParam defines model for webhookIdentifierPathParam.
type WebhookIdentifierPathParam string
// ArtifactDetailResponse defines model for ArtifactDetailResponse.
type ArtifactDetailResponse struct {
// Data Artifact Detail
@ -769,6 +913,24 @@ type ListRegistryResponse struct {
Status Status `json:"status"`
}
// ListWebhooksExecutionResponse defines model for ListWebhooksExecutionResponse.
type ListWebhooksExecutionResponse struct {
// Data A list of Harness Registries webhooks executions
Data ListWebhooksExecutions `json:"data"`
// Status Indicates if the request was successful or not
Status Status `json:"status"`
}
// ListWebhooksResponse defines model for ListWebhooksResponse.
type ListWebhooksResponse struct {
// Data A list of Harness Registries webhooks
Data ListWebhooks `json:"data"`
// Status Indicates if the request was successful or not
Status Status `json:"status"`
}
// NotFound defines model for NotFound.
type NotFound Error
@ -793,6 +955,24 @@ type Unauthenticated Error
// Unauthorized defines model for Unauthorized.
type Unauthorized Error
// WebhookExecutionResponse defines model for WebhookExecutionResponse.
type WebhookExecutionResponse struct {
// Data Harness Regstries Webhook Execution
Data WebhookExecution `json:"data"`
// Status Indicates if the request was successful or not
Status Status `json:"status"`
}
// WebhookResponse defines model for WebhookResponse.
type WebhookResponse struct {
// Data Harness Regstries Webhook
Data Webhook `json:"data"`
// Status Indicates if the request was successful or not
Status Status `json:"status"`
}
// CreateRegistryParams defines parameters for CreateRegistry.
type CreateRegistryParams struct {
// SpaceRef Unique space path
@ -901,6 +1081,33 @@ type GetClientSetupDetailsParams struct {
Version *VersionParam `form:"version,omitempty" json:"version,omitempty"`
}
// ListWebhooksParams defines parameters for ListWebhooks.
type ListWebhooksParams struct {
// Page Current page number
Page *PageNumber `form:"page,omitempty" json:"page,omitempty"`
// Size Number of items per page
Size *PageSize `form:"size,omitempty" json:"size,omitempty"`
// SortOrder sortOrder
SortOrder *SortOrder `form:"sort_order,omitempty" json:"sort_order,omitempty"`
// SortField sortField
SortField *SortField `form:"sort_field,omitempty" json:"sort_field,omitempty"`
// SearchTerm search Term.
SearchTerm *SearchTerm `form:"search_term,omitempty" json:"search_term,omitempty"`
}
// ListWebhookExecutionsParams defines parameters for ListWebhookExecutions.
type ListWebhookExecutionsParams struct {
// Page Current page number
Page *PageNumber `form:"page,omitempty" json:"page,omitempty"`
// Size Number of items per page
Size *PageSize `form:"size,omitempty" json:"size,omitempty"`
}
// GetArtifactStatsForSpaceParams defines parameters for GetArtifactStatsForSpace.
type GetArtifactStatsForSpaceParams struct {
// From Date. Format - MM/DD/YYYY
@ -976,6 +1183,12 @@ type ModifyRegistryJSONRequestBody RegistryRequest
// UpdateArtifactLabelsJSONRequestBody defines body for UpdateArtifactLabels for application/json ContentType.
type UpdateArtifactLabelsJSONRequestBody ArtifactLabelRequest
// CreateWebhookJSONRequestBody defines body for CreateWebhook for application/json ContentType.
type CreateWebhookJSONRequestBody WebhookRequest
// UpdateWebhookJSONRequestBody defines body for UpdateWebhook for application/json ContentType.
type UpdateWebhookJSONRequestBody WebhookRequest
// AsDockerArtifactDetailConfig returns the union data inside the ArtifactDetail as a DockerArtifactDetailConfig
func (t ArtifactDetail) AsDockerArtifactDetailConfig() (DockerArtifactDetailConfig, error) {
var body DockerArtifactDetailConfig

View File

@ -67,6 +67,7 @@ func NewAPIHandler(
auditService audit.Service,
spacePathStore corestore.SpacePathStore,
artifactStore store.ArtifactRepository,
webhooksRepository store.WebhooksRepository,
) APIHandler {
r := chi.NewRouter()
r.Use(audit.Middleware())
@ -87,6 +88,7 @@ func NewAPIHandler(
auditService,
spacePathStore,
artifactStore,
webhooksRepository,
)
handler := artifact.NewStrictHandler(apiController, []artifact.StrictMiddlewareFunc{})
muxHandler := artifact.HandlerFromMuxWithBaseURL(handler, r, baseURL)

View File

@ -57,6 +57,7 @@ func APIHandlerProvider(
auditService audit.Service,
spacePathStore corestore.SpacePathStore,
artifactStore store.ArtifactRepository,
webhooksRepository store.WebhooksRepository,
) harness.APIHandler {
return harness.NewAPIHandler(
repoDao,
@ -75,6 +76,7 @@ func APIHandlerProvider(
auditService,
spacePathStore,
artifactStore,
webhooksRepository,
)
}

View File

@ -516,3 +516,25 @@ type GenericBlobRepository interface {
Create(ctx context.Context, gb *types.GenericBlob) error
DeleteByID(ctx context.Context, id string) error
}
type WebhooksRepository interface {
Create(ctx context.Context, webhook *types.Webhook) error
GetByRegistryAndIdentifier(ctx context.Context, registryID int64, webhookIdentifier string) (*types.Webhook, error)
ListByRegistry(
ctx context.Context,
sortByField string,
sortByOrder string,
limit int,
offset int,
search string,
registryID int64,
) (*[]types.Webhook, error)
CountAllByRegistry(
ctx context.Context,
registryID int64,
search string,
) (int64, error)
Update(ctx context.Context, webhook *types.Webhook) error
DeleteByRegistryAndIdentifier(ctx context.Context, registryID int64, webhookIdentifier string) error
}

View File

@ -26,7 +26,7 @@ import (
"github.com/harness/gitness/registry/app/store"
"github.com/harness/gitness/registry/app/store/database/util"
"github.com/harness/gitness/registry/types"
gitness_store "github.com/harness/gitness/store"
gitnessstore "github.com/harness/gitness/store"
databaseg "github.com/harness/gitness/store/database"
"github.com/harness/gitness/store/database/dbtx"
@ -236,7 +236,6 @@ func (r registryDao) GetAll(
repoType string,
recursive bool,
) (repos *[]store.RegistryMetadata, err error) {
// Select only required fields
selectFields := `
r.registry_id AS registry_id,
r.registry_name AS reg_identifier,
@ -620,7 +619,7 @@ func (r registryDao) Update(ctx context.Context, registry *types.Registry) (err
}
if count == 0 {
return gitness_store.ErrVersionConflict
return gitnessstore.ErrVersionConflict
}
return nil

View File

@ -0,0 +1,453 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package database
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/harness/gitness/registry/app/api/openapi/contracts/artifact"
"github.com/harness/gitness/registry/app/pkg/commons"
"github.com/harness/gitness/registry/app/store"
"github.com/harness/gitness/registry/app/store/database/util"
"github.com/harness/gitness/registry/types"
"github.com/harness/gitness/registry/types/enum"
gitnessstore "github.com/harness/gitness/store"
"github.com/harness/gitness/store/database"
"github.com/harness/gitness/store/database/dbtx"
"github.com/guregu/null"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
)
const triggersSeparator = ","
var registryWebhooksFields = []string{
"registry_webhook_id",
"registry_webhook_version",
"registry_webhook_registry_id",
"registry_webhook_space_id",
"registry_webhook_created_by",
"registry_webhook_created",
"registry_webhook_updated",
"registry_webhook_scope",
"registry_webhook_identifier",
"registry_webhook_name",
"registry_webhook_description",
"registry_webhook_url",
"registry_webhook_secret_identifier",
"registry_webhook_secret_space_id",
"registry_webhook_enabled",
"registry_webhook_internal",
"registry_webhook_insecure",
"registry_webhook_triggers",
"registry_webhook_extra_headers",
"registry_webhook_latest_execution_result",
}
func NewWebhookDao(db *sqlx.DB) store.WebhooksRepository {
return &WebhookDao{
db: db,
}
}
type webhookDB struct {
ID int64 `db:"registry_webhook_id"`
Version int64 `db:"registry_webhook_version"`
RegistryID null.Int `db:"registry_webhook_registry_id"`
SpaceID null.Int `db:"registry_webhook_space_id"`
CreatedBy int64 `db:"registry_webhook_created_by"`
Created int64 `db:"registry_webhook_created"`
Updated int64 `db:"registry_webhook_updated"`
Scope int64 `db:"registry_webhook_scope"`
Internal bool `db:"registry_webhook_internal"`
Identifier string `db:"registry_webhook_identifier"`
Name string `db:"registry_webhook_name"`
Description string `db:"registry_webhook_description"`
URL string `db:"registry_webhook_url"`
SecretIdentifier sql.NullString `db:"registry_webhook_secret_identifier"`
SecretSpaceID sql.NullInt32 `db:"registry_webhook_secret_space_id"`
Enabled bool `db:"registry_webhook_enabled"`
Insecure bool `db:"registry_webhook_insecure"`
Triggers string `db:"registry_webhook_triggers"`
ExtraHeaders null.String `db:"registry_webhook_extra_headers"`
LatestExecutionResult null.String `db:"registry_webhook_latest_execution_result"`
}
type WebhookDao struct {
db *sqlx.DB
}
func (w WebhookDao) Create(ctx context.Context, webhook *types.Webhook) error {
const sqlQuery = `
INSERT INTO registry_webhooks (
registry_webhook_registry_id
,registry_webhook_space_id
,registry_webhook_created_by
,registry_webhook_created
,registry_webhook_updated
,registry_webhook_identifier
,registry_webhook_name
,registry_webhook_description
,registry_webhook_url
,registry_webhook_secret_identifier
,registry_webhook_secret_space_id
,registry_webhook_enabled
,registry_webhook_internal
,registry_webhook_insecure
,registry_webhook_triggers
,registry_webhook_latest_execution_result
,registry_webhook_extra_headers
,registry_webhook_scope
) values (
:registry_webhook_registry_id
,:registry_webhook_space_id
,:registry_webhook_created_by
,:registry_webhook_created
,:registry_webhook_updated
,:registry_webhook_identifier
,:registry_webhook_name
,:registry_webhook_description
,:registry_webhook_url
,:registry_webhook_secret_identifier
,:registry_webhook_secret_space_id
,:registry_webhook_enabled
,:registry_webhook_internal
,:registry_webhook_insecure
,:registry_webhook_triggers
,:registry_webhook_latest_execution_result
,:registry_webhook_extra_headers
,:registry_webhook_scope
) RETURNING registry_webhook_id`
db := dbtx.GetAccessor(ctx, w.db)
dbwebhook, err := mapToWebhookDB(webhook)
dbwebhook.Created = webhook.CreatedAt.UnixMilli()
dbwebhook.Updated = webhook.UpdatedAt.UnixMilli()
if err != nil {
return fmt.Errorf("failed to map registry webhook to internal db type: %w", err)
}
query, arg, err := db.BindNamed(sqlQuery, dbwebhook)
if err != nil {
return database.ProcessSQLErrorf(ctx, err, "Failed to registry bind webhook object")
}
if err = db.QueryRowContext(ctx, query, arg...).Scan(&webhook.ID); err != nil {
return database.ProcessSQLErrorf(ctx, err, "Insert query failed")
}
return nil
}
func (w WebhookDao) GetByRegistryAndIdentifier(
ctx context.Context,
registryID int64,
webhookIdentifier string,
) (*types.Webhook, error) {
query := database.Builder.Select(registryWebhooksFields...).
From("registry_webhooks").
Where("registry_webhook_registry_id = ? AND registry_webhook_identifier = ?", registryID, webhookIdentifier)
sqlQuery, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Failed to convert query to sql")
}
db := dbtx.GetAccessor(ctx, w.db)
dst := new(webhookDB)
if err = db.GetContext(ctx, dst, sqlQuery, args...); err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to get webhook detail")
}
return mapToWebhook(dst)
}
func (w WebhookDao) ListByRegistry(
ctx context.Context,
sortByField string,
sortByOrder string,
limit int,
offset int,
search string,
registryID int64,
) (*[]types.Webhook, error) {
query := database.Builder.Select(registryWebhooksFields...).
From("registry_webhooks").
Where("registry_webhook_registry_id = ?", registryID)
if search != "" {
query = query.Where("registry_webhook_name LIKE ?", "%"+search+"%")
}
validSortFields := map[string]string{
"name": "registry_webhook_name",
}
validSortByField := validSortFields[sortByField]
if validSortByField != "" {
query = query.OrderBy(fmt.Sprintf("%s %s", validSortByField, sortByOrder))
}
query = query.Limit(uint64(limit)).Offset(uint64(offset))
sqlQuery, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Failed to convert query to sql")
}
db := dbtx.GetAccessor(ctx, w.db)
var dst []*webhookDB
if err = db.SelectContext(ctx, &dst, sqlQuery, args...); err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to list webhooks details")
}
return mapToWebhooksList(dst)
}
func (w WebhookDao) CountAllByRegistry(
ctx context.Context,
registryID int64,
search string,
) (int64, error) {
stmt := database.Builder.Select("COUNT(*)").
From("registry_webhooks").
Where("registry_webhook_registry_id = ?", registryID)
if !commons.IsEmpty(search) {
stmt = stmt.Where("registry_webhook_name LIKE ?", "%"+search+"%")
}
sqlQuery, args, err := stmt.ToSql()
if err != nil {
return -1, errors.Wrap(err, "Failed to convert query to sql")
}
db := dbtx.GetAccessor(ctx, w.db)
var count int64
err = db.QueryRowContext(ctx, sqlQuery, args...).Scan(&count)
if err != nil {
return 0, database.ProcessSQLErrorf(ctx, err, "Failed executing count query")
}
return count, nil
}
func (w WebhookDao) Update(ctx context.Context, webhook *types.Webhook) error {
var sqlQuery = " UPDATE registry_webhooks SET " +
util.GetSetDBKeys(webhookDB{},
"registry_webhook_identifier",
"registry_webhook_registry_id",
"registry_webhook_created",
"registry_webhook_created_by",
"registry_webhook_version",
"registry_webhook_internal") +
", registry_webhook_version = registry_webhook_version + 1" +
" WHERE registry_webhook_identifier = :registry_webhook_identifier" +
" AND registry_webhook_registry_id = :registry_webhook_registry_id"
dbWebhook, err := mapToWebhookDB(webhook)
dbWebhook.Updated = webhook.UpdatedAt.UnixMilli()
if err != nil {
return err
}
dbWebhook.Updated = time.Now().UnixMilli()
db := dbtx.GetAccessor(ctx, w.db)
query, arg, err := db.BindNamed(sqlQuery, dbWebhook)
if err != nil {
return database.ProcessSQLErrorf(ctx, err, "Failed to bind registry webhook object")
}
result, err := db.ExecContext(ctx, query, arg...)
if err != nil {
return database.ProcessSQLErrorf(ctx, err, "Failed to update registry webhook")
}
count, err := result.RowsAffected()
if err != nil {
return database.ProcessSQLErrorf(ctx, err, "Failed to get number of updated rows")
}
if count == 0 {
return gitnessstore.ErrVersionConflict
}
return nil
}
func (w WebhookDao) DeleteByRegistryAndIdentifier(
ctx context.Context,
registryID int64,
webhookIdentifier string,
) error {
sqlQuery := database.Builder.Delete("registry_webhooks").
Where("registry_webhook_identifier = ? AND registry_webhook_registry_id = ?", webhookIdentifier, registryID)
query, args, err := sqlQuery.ToSql()
if err != nil {
return fmt.Errorf("failed to convert purge registry_webhooks query to sql: %w", err)
}
db := dbtx.GetAccessor(ctx, w.db)
_, err = db.ExecContext(ctx, query, args...)
if err != nil {
return database.ProcessSQLErrorf(ctx, err, "the delete registry_webhooks query failed")
}
return nil
}
func mapToWebhookDB(webhook *types.Webhook) (*webhookDB, error) {
if webhook.CreatedAt.IsZero() {
webhook.CreatedAt = time.Now()
}
webhook.UpdatedAt = time.Now()
dBwebhook := &webhookDB{
ID: webhook.ID,
Version: webhook.Version,
CreatedBy: webhook.CreatedBy,
Identifier: webhook.Identifier,
Scope: webhook.Scope,
Name: webhook.Name,
Description: webhook.Description,
URL: webhook.URL,
SecretIdentifier: util.GetEmptySQLString(webhook.SecretIdentifier),
SecretSpaceID: util.GetEmptySQLInt32(webhook.SecretSpaceID),
Enabled: webhook.Enabled,
Insecure: webhook.Insecure,
Internal: webhook.Internal,
Triggers: triggersToString(webhook.Triggers),
ExtraHeaders: null.StringFrom(structListToString(webhook.ExtraHeaders)),
LatestExecutionResult: null.StringFromPtr((*string)(webhook.LatestExecutionResult)),
}
switch webhook.ParentType {
case enum.WebhookParentRegistry:
dBwebhook.RegistryID = null.IntFrom(webhook.ParentID)
case enum.WebhookParentSpace:
dBwebhook.SpaceID = null.IntFrom(webhook.ParentID)
default:
return nil, fmt.Errorf("webhook parent type %q is not supported", webhook.ParentType)
}
return dBwebhook, nil
}
func mapToWebhook(webhookDB *webhookDB) (*types.Webhook, error) {
webhook := &types.Webhook{
ID: webhookDB.ID,
Version: webhookDB.Version,
CreatedBy: webhookDB.CreatedBy,
CreatedAt: time.UnixMilli(webhookDB.Created),
UpdatedAt: time.UnixMilli(webhookDB.Updated),
Scope: webhookDB.Scope,
Identifier: webhookDB.Identifier,
Name: webhookDB.Name,
Description: webhookDB.Description,
URL: webhookDB.URL,
Enabled: webhookDB.Enabled,
Internal: webhookDB.Internal,
Insecure: webhookDB.Insecure,
Triggers: triggersFromString(webhookDB.Triggers),
ExtraHeaders: stringToStructList(webhookDB.ExtraHeaders.String),
LatestExecutionResult: (*artifact.WebhookExecResult)(webhookDB.LatestExecutionResult.Ptr()),
}
if webhookDB.SecretIdentifier.Valid {
webhook.SecretIdentifier = webhookDB.SecretIdentifier.String
}
if webhookDB.SecretSpaceID.Valid {
webhook.SecretSpaceID = int(webhookDB.SecretSpaceID.Int32)
}
switch {
case webhookDB.RegistryID.Valid && webhookDB.SpaceID.Valid:
return nil, fmt.Errorf("both registryID and spaceID are set for hook %d", webhookDB.ID)
case webhookDB.RegistryID.Valid:
webhook.ParentType = enum.WebhookParentRegistry
webhook.ParentID = webhookDB.RegistryID.Int64
case webhookDB.SpaceID.Valid:
webhook.ParentType = enum.WebhookParentSpace
webhook.ParentID = webhookDB.SpaceID.Int64
default:
return nil, fmt.Errorf("neither registryID nor spaceID are set for hook %d", webhookDB.ID)
}
return webhook, nil
}
func triggersToString(triggers []artifact.Trigger) string {
rawTriggers := make([]string, len(triggers))
for i := range triggers {
rawTriggers[i] = string(triggers[i])
}
return strings.Join(rawTriggers, triggersSeparator)
}
func triggersFromString(triggersString string) []artifact.Trigger {
if triggersString == "" {
return []artifact.Trigger{}
}
rawTriggers := strings.Split(triggersString, triggersSeparator)
triggers := make([]artifact.Trigger, len(rawTriggers))
for i, rawTrigger := range rawTriggers {
triggers[i] = artifact.Trigger(rawTrigger)
}
return triggers
}
// Convert a list of ExtraHeaders structs to a JSON string.
func structListToString(headers []artifact.ExtraHeader) string {
jsonData, err := json.Marshal(headers)
if err != nil {
return ""
}
return string(jsonData)
}
// Convert a JSON string back to a list of ExtraHeaders structs.
func stringToStructList(jsonStr string) []artifact.ExtraHeader {
var headers []artifact.ExtraHeader
err := json.Unmarshal([]byte(jsonStr), &headers)
if err != nil {
return nil
}
return headers
}
func mapToWebhooksList(
dst []*webhookDB,
) (*[]types.Webhook, error) {
webhooks := make([]types.Webhook, 0, len(dst))
for _, d := range dst {
webhook, err := mapToWebhook(d)
if err != nil {
return nil, err
}
webhooks = append(webhooks, *webhook)
}
return &webhooks, nil
}

View File

@ -71,6 +71,10 @@ func ProvideManifestDao(sqlDB *sqlx.DB, mtRepository store.MediaTypesRepository)
return NewManifestDao(sqlDB, mtRepository)
}
func ProvideWebhookDao(sqlDB *sqlx.DB) store.WebhooksRepository {
return NewWebhookDao(sqlDB)
}
func ProvideManifestRefDao(db *sqlx.DB) store.ManifestReferenceRepository {
return NewManifestReferenceDao(db)
}
@ -113,4 +117,5 @@ var WireSet = wire.NewSet(
ProvideBandwidthStatDao,
ProvideNodeDao,
ProvideGenericBlobDao,
ProvideWebhookDao,
)

View File

@ -0,0 +1,33 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package enum
import (
"golang.org/x/exp/constraints"
"golang.org/x/exp/slices"
)
func toInterfaceSlice[T interface{}](vals []T) []interface{} {
res := make([]interface{}, len(vals))
for i := range vals {
res[i] = vals[i]
}
return res
}
func sortEnum[T constraints.Ordered](slice []T) []T {
slices.Sort(slice)
return slice
}

View File

@ -0,0 +1,33 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package enum
// RegistryWebhookParent defines different types of parents of a webhook.
type RegistryWebhookParent string
func (RegistryWebhookParent) Enum() []interface{} { return toInterfaceSlice(webhookParents) }
const (
// WebhookParentRegistry describes a registry as webhook owner.
WebhookParentRegistry RegistryWebhookParent = "registry"
// WebhookParentSpace describes a space as webhook owner.
WebhookParentSpace RegistryWebhookParent = "space"
)
var webhookParents = sortEnum([]RegistryWebhookParent{
WebhookParentRegistry,
WebhookParentSpace,
})

46
registry/types/webhook.go Normal file
View File

@ -0,0 +1,46 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package types
import (
"time"
"github.com/harness/gitness/registry/app/api/openapi/contracts/artifact"
"github.com/harness/gitness/registry/types/enum"
)
// Webhook DTO object.
type Webhook struct {
ID int64
Version int64
ParentType enum.RegistryWebhookParent
ParentID int64
CreatedBy int64
CreatedAt time.Time
UpdatedAt time.Time
Scope int64
Identifier string
Name string
Description string
URL string
SecretIdentifier string
SecretSpaceID int
Enabled bool
Insecure bool
Internal bool
ExtraHeaders []artifact.ExtraHeader
Triggers []artifact.Trigger
LatestExecutionResult *artifact.WebhookExecResult
}