├── test ├── resources │ ├── acceptance-expected │ │ ├── health.json │ │ ├── service-promoters.json │ │ ├── repositories-empty.json │ │ ├── service-notfound.json │ │ ├── webhook-invalid.json │ │ ├── forbidden.json │ │ ├── owner-invalid-syntax.json │ │ ├── owner-notfound-deleteme.json │ │ ├── owner-notfound.json │ │ ├── service-invalid-syntax.json │ │ ├── owner-delete-invalid-values.json │ │ ├── repository-invalid-syntax.json │ │ ├── service-delete-invalid-values.json │ │ ├── repository-delete-invalid-values.json │ │ ├── repository-notfound.json │ │ ├── service-notfound-doesnotexist.json │ │ ├── service-create-owner-missing.json │ │ ├── service-update-owner-missing.json │ │ ├── owner-notfound-migration-excellence.json │ │ ├── repository-create-owner-missing.json │ │ ├── repository-update-owner-missing.json │ │ ├── owner-create-invalid-values.json │ │ ├── repository-notfound-doesnotexist.json │ │ ├── service-notfound-someservicebackend.json │ │ ├── repositories-query-owner-invalid.json │ │ ├── repository-notfound-karmawrapper.json │ │ ├── repository-create-invalid-values.json │ │ ├── bad-gateway.json │ │ ├── owner-invalid.json │ │ ├── owner-delete-conflict.json │ │ ├── repositories-query-service-invalid.json │ │ ├── repositories-query-name-invalid.json │ │ ├── unauthorized.json │ │ ├── repository-delete-referenced.json │ │ ├── owner-invalid-alias.json │ │ ├── service-invalid.json │ │ ├── service-invalid-name.json │ │ ├── service-update-invalid-values.json │ │ ├── receive-hook-declined.json │ │ ├── service-update-repo-missing.json │ │ ├── owner-update-invalid-values.json │ │ ├── repository-update-invalid-values.json │ │ ├── owner-patch-invalid-values.json │ │ ├── repository-patch-invalid-values.json │ │ ├── service-create-repo-missing.json │ │ ├── repositories-query-type-invalid.json │ │ ├── service-patch-invalid-values.json │ │ ├── repository-update-referenced.json │ │ ├── service-create-invalid-values.json │ │ ├── owner-create.json │ │ ├── owner-update.json │ │ ├── repository-invalid.json │ │ ├── repository-invalid-key.json │ │ ├── repository-unchanged.json │ │ ├── repository-patch-conflict.json │ │ ├── repository-patch-newowner.json │ │ ├── repository-create-duplicate.json │ │ ├── repository-update-conflict.json │ │ ├── owner-unchanged.json │ │ ├── owner.json │ │ ├── owner-patch.json │ │ ├── owner-create-duplicate.json │ │ ├── owner-patch-conflict.json │ │ ├── owner-update-conflict.json │ │ ├── service-unchanged.json │ │ ├── service.json │ │ ├── service-original.json │ │ ├── service-patch-conflict.json │ │ ├── service-update-conflict.json │ │ ├── service-create-duplicate.json │ │ ├── service-create.json │ │ ├── service-create-crossref.json │ │ ├── service-patch-crossref.json │ │ ├── service-patch-crossref-ownerchange.json │ │ ├── service-patch.json │ │ ├── service-update.json │ │ ├── service-patch-newowner.json │ │ ├── service-update-newowner.json │ │ ├── repository-patch.json │ │ ├── service-patch-spec.json │ │ ├── repositories-filtered-type.json │ │ ├── repository-create.json │ │ ├── owners.json │ │ ├── repository-update.json │ │ ├── repository-update-newowner.json │ │ ├── repository-create-cache.json │ │ ├── repository-update-cache.json │ │ ├── repository-update-newowner-cache.json │ │ ├── services.json │ │ ├── repository.json │ │ ├── repository-expanded-groups.json │ │ └── repositories-filtered-service.json │ ├── invalid-config-syntax.yaml │ ├── invalid-config-values.yaml │ ├── valid-config-unique.yaml │ └── recordings │ │ └── github │ │ └── request_get_repos-some-org-some-repo-contents-owners-test-owner-dev-owner.info.yaml_8a051734.json ├── acceptance │ ├── dummy.go │ ├── util_main_test.go │ ├── util_notifier_test.go │ ├── startup_acc_test.go │ ├── webhook_github_acc_test.go │ └── util_auth_test.go └── mock │ ├── authprovidermock │ └── authprovidermock.go │ ├── vaultmock │ └── vault.go │ ├── notifiermock │ └── notifierclientmock.go │ ├── githubmock │ └── githubmock.go │ ├── kafkamock │ └── kafka.go │ ├── idpmock │ └── idp.go │ └── cachemock │ └── cachemock.go ├── internal ├── util │ └── util.go ├── acorn │ ├── service │ │ ├── webhookshandlerint.go │ │ ├── triggerint.go │ │ ├── validatorint.go │ │ ├── ownersint.go │ │ ├── servicesint.go │ │ ├── repositoriesint.go │ │ ├── mapperint.go │ │ └── updaterint.go │ ├── application │ │ ├── serverint.go │ │ └── appint.go │ ├── controller │ │ ├── webhookctlint.go │ │ ├── ownerctlint.go │ │ ├── servicectlint.go │ │ └── repositoryctlint.go │ ├── repository │ │ ├── hostipint.go │ │ ├── authProviderInt.go │ │ ├── notifierint.go │ │ ├── idpint.go │ │ ├── github.go │ │ ├── kafkaint.go │ │ └── metadataint.go │ └── errors │ │ ├── githookerror │ │ └── error.go │ │ ├── nochangeserror │ │ └── error.go │ │ ├── inputvalidationerror │ │ └── error.go │ │ └── httperror │ │ └── error.go ├── service │ ├── mapper │ │ ├── timestamp.go │ │ ├── jiraissue_test.go │ │ ├── jiraissue.go │ │ └── owner.go │ ├── updater │ │ ├── timestamp.go │ │ └── metadata.go │ ├── util │ │ ├── DuplicateHelper.go │ │ ├── UserGroupHelper_test.go │ │ └── UserGroupHelper.go │ └── check │ │ └── yamlwalker_format.go ├── types │ └── types.go ├── repository │ ├── cache │ │ ├── ownercache.go │ │ ├── servicecache.go │ │ ├── repositorycache.go │ │ └── cache.go │ ├── config │ │ └── accessor_test.go │ ├── hostip │ │ └── hostip.go │ ├── notifier │ │ └── client │ │ │ └── notifier │ │ │ └── client.go │ ├── github │ │ └── github.go │ └── authProvider │ │ ├── authProvider.go │ │ └── authProvider_test.go └── web │ ├── util │ ├── paramhelper.go │ └── responsehelper.go │ ├── server │ └── metricsserver.go │ └── controller │ └── webhookctl │ └── webhookctl.go ├── docs └── architecture-export.png ├── Makefile ├── .gitignored └── info.md ├── main.go ├── .goreleaser.yaml ├── api ├── generated_model_merge_strategy.go ├── generated_model_post_promote.go ├── generated_model_link.go ├── generated_model_service_promoters_dto.go ├── generated_model_deletion_dto.go ├── generated_model_exclude_merge_check_user_dto.go ├── generated_model_repository_configuration_default_task_dto.go ├── generated_model_health_component.go ├── generated_model_ref_protections.go ├── generated_model_owner_list_dto.go ├── generated_model_quicklink.go ├── generated_model_service_list_dto.go ├── generated_model_repository_list_dto.go ├── generated_model_error_dto.go ├── generated_model_notification_payload.go ├── generated_model_repository_configuration_dto_merge_config.go ├── generated_model_pull_requests.go ├── generated_model_repository_configuration_access_key_dto.go ├── generated_model_repository_configuration_webhooks_dto.go ├── generated_model_notification.go ├── generated_model_repository_configuration_webhook_dto.go ├── generated_model_binary.go ├── generated_model_service_spec_dto.go ├── generated_model_condition_reference_dto.go ├── generated_model_ref_protections_tags.go ├── generated_model_protected_ref.go ├── generated_model_repository_create_dto.go ├── generated_model_ref_protections_branches.go ├── generated_model_repository_patch_dto.go ├── generated_model_repository_dto.go ├── generated_model_owner_create_dto.go ├── generated_model_owner_patch_dto.go ├── generated_model_owner_dto.go ├── generated_model_service_create_dto.go ├── generated_model_service_dto.go └── generated_model_service_patch_dto.go ├── Dockerfile ├── .github ├── pull_request_template.md ├── dependabot.yaml └── workflows │ ├── semantic-commit-message-check.yaml │ └── go.yaml ├── pkg └── recorder │ └── recorder.go ├── LICENSE ├── .gitignore ├── api-generator └── generate.sh └── local-config.template.yaml /test/resources/acceptance-expected/health.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": "UP" 3 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-promoters.json: -------------------------------------------------------------------------------- 1 | { 2 | "promoters": [ 3 | ] 4 | } -------------------------------------------------------------------------------- /internal/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | func Ptr[T any](input T) *T { 4 | return &input 5 | } 6 | -------------------------------------------------------------------------------- /docs/architecture-export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Interhyp/metadata-service/HEAD/docs/architecture-export.png -------------------------------------------------------------------------------- /test/resources/invalid-config-syntax.yaml: -------------------------------------------------------------------------------- 1 | APPLICATION_NAME: metadata 2 | - some invalid syntax stuff 3 | - blah 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: generate 2 | generate: 3 | @./api-generator/generate.sh 4 | 5 | .PHONY: test 6 | test: 7 | @go test ./... -------------------------------------------------------------------------------- /test/acceptance/dummy.go: -------------------------------------------------------------------------------- 1 | package acceptance 2 | 3 | // go wants a non-test go file in every package, or test coverage cannot run 4 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repositories-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "repositories": {}, 3 | "timeStamp": "2022-11-06T18:14:10Z" 4 | } -------------------------------------------------------------------------------- /.gitignored/info.md: -------------------------------------------------------------------------------- 1 | This folder is ignored via .gitignore. Use it to store files, which shall not be committed, i.e. temp files, test requests. -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-notfound.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "service unicorn not found", 3 | "message": "service.notfound", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/webhook-invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "parse payload error", 3 | "message": "webhook.payload.invalid", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/Interhyp/metadata-service/internal/web/app" 5 | "os" 6 | ) 7 | 8 | func main() { 9 | os.Exit(app.New().Run()) 10 | } 11 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/forbidden.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "you are not authorized for this operation", 3 | "message": "forbidden", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/owner-invalid-syntax.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "body failed to parse", 3 | "message": "owner.invalid.body", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/owner-notfound-deleteme.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "owner deleteme not found", 3 | "message": "owner.notfound", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/owner-notfound.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "owner does-not-exist not found", 3 | "message": "owner.notfound", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-invalid-syntax.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "body failed to parse", 3 | "message": "service.invalid.body", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/owner-delete-invalid-values.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "body failed to parse", 3 | "message": "deletion.invalid.body", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-invalid-syntax.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "body failed to parse", 3 | "message": "repository.invalid.body", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-delete-invalid-values.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "body failed to parse", 3 | "message": "deletion.invalid.body", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-delete-invalid-values.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "body failed to parse", 3 | "message": "deletion.invalid.body", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-notfound.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "repository unicorn.helm-chart not found", 3 | "message": "repository.notfound", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-notfound-doesnotexist.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "service does-not-exist not found", 3 | "message": "service.notfound", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-create-owner-missing.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "no such owner: not-there", 3 | "message": "service.invalid.missing.owner", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-update-owner-missing.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "no such owner: not-there", 3 | "message": "service.invalid.missing.owner", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/owner-notfound-migration-excellence.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "owner migration-excellence not found", 3 | "message": "owner.notfound", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-create-owner-missing.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "no such owner: not-there", 3 | "message": "repository.invalid.missing.owner", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-update-owner-missing.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "no such owner: not-there", 3 | "message": "repository.invalid.missing.owner", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/owner-create-invalid-values.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "validation error: field contact is mandatory", 3 | "message": "owner.invalid.values", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-notfound-doesnotexist.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "repository does-not-exist.api not found", 3 | "message": "repository.notfound", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-notfound-someservicebackend.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "service some-service-backend not found", 3 | "message": "service.notfound", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repositories-query-owner-invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "owner filter must match ^[a-z](-?[a-z0-9]+)*$", 3 | "message": "owner.invalid.filter", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-notfound-karmawrapper.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "repository karma-wrapper.helm-chart not found", 3 | "message": "repository.notfound", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-create-invalid-values.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "validation error: field owner is mandatory", 3 | "message": "repository.invalid.values", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/bad-gateway.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "the git server is currently unavailable or failed to service the request", 3 | "message": "downstream.unavailable", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/owner-invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "owner alias must match ^[a-z](-?[a-z0-9]+)*$ and is not allowed to match ^$", 3 | "message": "owner.invalid.alias", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /internal/acorn/service/webhookshandlerint.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | type WebhooksHandler interface { 9 | HandleEvent(ctx context.Context, r *http.Request) error 10 | } 11 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/owner-delete-conflict.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "this owner still has services or repositories and cannot be deleted", 3 | "message": "owner.conflict.notempty", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repositories-query-service-invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "service name filter must match ^[a-z](-?[a-z0-9]+)*$", 3 | "message": "service.invalid.filter", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /internal/acorn/application/serverint.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import "context" 4 | 5 | type Server interface { 6 | IsServer() bool 7 | 8 | Setup() error 9 | 10 | WireUp(ctx context.Context) 11 | 12 | Run() error 13 | } 14 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repositories-query-name-invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "repository name filter must match ^[a-z](-?[a-z0-9]+)*$", 3 | "message": "repository.invalid.filter.name", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /internal/service/mapper/timestamp.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | var TimeStampFormat = "2006-01-02T15:04:05Z" 8 | 9 | func timeStamp(t time.Time) string { 10 | return t.UTC().Format(TimeStampFormat) 11 | } 12 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/unauthorized.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "missing or invalid Authorization header (JWT bearer token expected) or token invalid or expired", 3 | "message": "unauthorized", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-delete-referenced.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "this repository is still being referenced by a service and cannot be deleted", 3 | "message": "repository.conflict.referenced", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/owner-invalid-alias.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "owner alias must match ^[a-z](-?[a-z0-9]+)*$, is not allowed to match ^$ and may have up to 28 characters", 3 | "message": "owner.invalid.alias", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "service name must match ^[a-z](-?[a-z0-9]+)*$, is not allowed to match -service$ and may have up to 28 characters", 3 | "message": "service.invalid.name", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-invalid-name.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "service name must match ^[a-z](-?[a-z0-9]+)*$, is not allowed to match -service$ and may have up to 28 characters", 3 | "message": "service.invalid.name", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-update-invalid-values.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "validation error: field owner is mandatory, field alertTarget is mandatory, field timeStamp is mandatory for updates", 3 | "message": "service.invalid.values", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/receive-hook-declined.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "git hook declined the commit - most likely your JIRA issue (INVALID-12345) does not exist, has wrong type, or wrong status", 3 | "message": "push.receive.hook.declined", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-update-repo-missing.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "validation error: you referenced a repository that does not exist: no such instance: some-service-backend.api", 3 | "message": "service.invalid.missing.repository", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /internal/service/updater/timestamp.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import "time" 4 | 5 | var TimeStampFormat = "2006-01-02T15:04:05Z" 6 | 7 | func timeStamp(t time.Time) string { 8 | return t.UTC().Format(TimeStampFormat) 9 | } 10 | 11 | // TODO set list timestamps when full update is done 12 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/owner-update-invalid-values.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "validation error: field contact is mandatory, field commitHash is mandatory for updates, field timeStamp is mandatory for updates", 3 | "message": "owner.invalid.values", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-update-invalid-values.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "validation error: field url is mandatory, field commitHash is mandatory for updates, field timeStamp is mandatory for updates", 3 | "message": "repository.invalid.values", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /internal/acorn/service/triggerint.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | // Trigger triggers update runs in Updater. 4 | // 5 | // Trigger events occur on initial app startup (before it becomes healthy), and periodically 6 | type Trigger interface { 7 | IsTrigger() bool 8 | Setup() error 9 | Teardown() 10 | } 11 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/owner-patch-invalid-values.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "validation error: field contact cannot be set to empty, field commitHash is mandatory for patching, field timeStamp is mandatory for patching", 3 | "message": "owner.invalid.values", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-patch-invalid-values.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "validation error: field owner is mandatory, field commitHash is mandatory for patching, field timeStamp is mandatory for patching", 3 | "message": "repository.invalid.values", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-create-repo-missing.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "validation error: you referenced a repository that does not exist: no such instance: post-service-invalid-repo.helm-deployment", 3 | "message": "service.invalid.missing.repository", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repositories-query-type-invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "repository type filter must match ^(api|helm-chart|helm-deployment|implementation|terraform-module|javascript-module|none)(-generator)?$", 3 | "message": "repository.invalid.filter.type", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-patch-invalid-values.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "validation error: field owner is mandatory, field alertTarget is mandatory, field commitHash is mandatory for patching, field timeStamp is mandatory for patching", 3 | "message": "service.invalid.values", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-update-referenced.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "this repository is being referenced in a service, you cannot change its owner directly - you can change the owner of the service and this will move it along", 3 | "message": "repository.conflict.referenced", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /internal/acorn/controller/webhookctlint.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "github.com/go-chi/chi/v5" 6 | ) 7 | 8 | // WebhookController provides a simple git webhook endpoint 9 | type WebhookController interface { 10 | IsWebhookController() bool 11 | 12 | WireUp(ctx context.Context, router chi.Router) 13 | } 14 | -------------------------------------------------------------------------------- /internal/acorn/controller/ownerctlint.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "github.com/go-chi/chi/v5" 6 | ) 7 | 8 | // OwnerController provides endpoints for managing owner information 9 | type OwnerController interface { 10 | IsOwnerController() bool 11 | 12 | WireUp(ctx context.Context, router chi.Router) 13 | } 14 | -------------------------------------------------------------------------------- /internal/acorn/controller/servicectlint.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "github.com/go-chi/chi/v5" 6 | ) 7 | 8 | // ServiceController provides endpoints for managing service information 9 | type ServiceController interface { 10 | IsServiceController() bool 11 | 12 | WireUp(ctx context.Context, router chi.Router) 13 | } 14 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-create-invalid-values.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "validation error: repository key must have acceptable name and type combination (allowed types: api implementation helm-deployment), and for helm-deployment the name must match the service name", 3 | "message": "service.invalid.values", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /internal/acorn/controller/repositoryctlint.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "github.com/go-chi/chi/v5" 6 | ) 7 | 8 | // RepositoryController provides endpoints for managing repository information 9 | type RepositoryController interface { 10 | IsRepositoryController() bool 11 | 12 | WireUp(ctx context.Context, router chi.Router) 13 | } 14 | -------------------------------------------------------------------------------- /internal/service/util/DuplicateHelper.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | func RemoveDuplicateStr(strSlice []string) []string { 4 | allKeys := make(map[string]bool) 5 | list := make([]string, 0) 6 | for _, item := range strSlice { 7 | if _, value := allKeys[item]; !value { 8 | allKeys[item] = true 9 | list = append(list, item) 10 | } 11 | } 12 | return list 13 | } 14 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/owner-create.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a2430000000000", 3 | "contact": "somebody@some-organisation.com", 4 | "defaultJiraProject": "JIRA", 5 | "jiraIssue": "ISSUE-2345", 6 | "productOwner": "kschlangenheld", 7 | "teamsChannelURL": "https://teams.microsoft.com/l/channel/somechannel", 8 | "timeStamp": "2022-11-06T18:14:10Z" 9 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/owner-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a2430000000000", 3 | "contact": "somebody@some-organisation.com", 4 | "defaultJiraProject": "JIRA", 5 | "jiraIssue": "ISSUE-2345", 6 | "productOwner": "kschlangenheld", 7 | "teamsChannelURL": "https://teams.microsoft.com/l/channel/somechannel", 8 | "timeStamp": "2022-11-06T18:14:10Z" 9 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "repository name must match ^[a-z](-?[a-z0-9]+)*$; is not allowed to match ^$ and may have up to 64 characters; repository type must be one of [implementation deployment api helm-chart] and name and type must be separated by a . character", 3 | "message": "repository.invalid", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-invalid-key.json: -------------------------------------------------------------------------------- 1 | { 2 | "details": "repository name must match ^[a-z](-?[a-z0-9]+)*$, is not allowed to match ^$ and may have up to 64 characters; repository type must be one of [implementation helm-deployment api helm-chart] and name and type must be separated by a . character", 3 | "message": "repository.invalid", 4 | "timestamp": "2022-11-06T18:14:10Z" 5 | } -------------------------------------------------------------------------------- /internal/acorn/repository/hostipint.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import "net" 4 | 5 | // HostIP interacts with the local network interfaces. 6 | type HostIP interface { 7 | IsHostIP() bool 8 | 9 | Setup() error 10 | 11 | // ObtainLocalIp gets the first non-localhost ipv4 address from your interfaces. 12 | // 13 | // In a k8s deployment, that'll be the pod ip. 14 | ObtainLocalIp() (net.IP, error) 15 | } 16 | -------------------------------------------------------------------------------- /internal/acorn/service/validatorint.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "github.com/google/go-github/v70/github" 6 | ) 7 | 8 | type Check interface { 9 | IsValidator() bool 10 | PerformValidationCheckRun(ctx context.Context, owner, repo, sha string) error 11 | PerformRequestedAction(ctx context.Context, requestedAction string, checkRun *github.CheckRun, requestingUser *github.User) error 12 | } 13 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-unchanged.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 3 | "configuration": { 4 | "branchNameRegex": "testing_.*" 5 | }, 6 | "jiraIssue": "", 7 | "mainline": "master", 8 | "owner": "some-owner", 9 | "timeStamp": "2022-11-06T18:14:10Z", 10 | "type": "helm-chart", 11 | "url": "ssh://git@bitbucket.some-organisation.com:7999/helm/karma-wrapper.git" 12 | } -------------------------------------------------------------------------------- /internal/acorn/repository/authProviderInt.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "github.com/go-git/go-git/v5/plumbing/transport" 6 | ) 7 | 8 | // AuthProvider is an AuthProvider business logic component. 9 | type AuthProvider interface { 10 | IsAuthProvider() bool 11 | 12 | Setup() error 13 | 14 | SetupProvider(ctx context.Context) error 15 | 16 | ProvideAuth(ctx context.Context) transport.AuthMethod 17 | } 18 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-patch-conflict.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 3 | "configuration": { 4 | "branchNameRegex": "testing_.*" 5 | }, 6 | "jiraIssue": "ISSUE-0000", 7 | "mainline": "master", 8 | "owner": "some-owner", 9 | "timeStamp": "2022-11-06T18:14:10Z", 10 | "type": "helm-chart", 11 | "url": "ssh://git@bitbucket.some-organisation.com:7999/helm/karma-wrapper.git" 12 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-patch-newowner.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a2430000000000", 3 | "configuration": { 4 | "branchNameRegex": "testing_.*" 5 | }, 6 | "jiraIssue": "ISSUE-2345", 7 | "mainline": "master", 8 | "owner": "deleteme", 9 | "timeStamp": "2022-11-06T18:14:10Z", 10 | "type": "helm-chart", 11 | "url": "ssh://git@bitbucket.some-organisation.com:7999/helm/karma-wrapper.git" 12 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-create-duplicate.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 3 | "configuration": { 4 | "branchNameRegex": "testing_.*" 5 | }, 6 | "jiraIssue": "ISSUE-0000", 7 | "mainline": "master", 8 | "owner": "some-owner", 9 | "timeStamp": "2022-11-06T18:14:10Z", 10 | "type": "helm-chart", 11 | "url": "ssh://git@bitbucket.some-organisation.com:7999/helm/karma-wrapper.git" 12 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-update-conflict.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 3 | "configuration": { 4 | "branchNameRegex": "testing_.*" 5 | }, 6 | "jiraIssue": "ISSUE-0000", 7 | "mainline": "master", 8 | "owner": "some-owner", 9 | "timeStamp": "2022-11-06T18:14:10Z", 10 | "type": "helm-chart", 11 | "url": "ssh://git@bitbucket.some-organisation.com:7999/helm/karma-wrapper.git" 12 | } -------------------------------------------------------------------------------- /internal/acorn/errors/githookerror/error.go: -------------------------------------------------------------------------------- 1 | package githookerror 2 | 3 | import "strings" 4 | 5 | const hookError = "pre-receive hook declined" 6 | 7 | // Is checks that an error is a git hook error. 8 | // 9 | // These errors occur during push operations. 10 | // 11 | // Unfortunately, we have to decide this by the error message, as go-git just uses fmt.Errorf(). 12 | func Is(err error) bool { 13 | return err != nil && strings.Contains(err.Error(), hookError) 14 | } 15 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/owner-unchanged.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 3 | "contact": "somebody@some-organisation.com", 4 | "defaultJiraProject": "ISSUE", 5 | "groups": { 6 | "users": [ 7 | "some-other-user", 8 | "a-very-special-user" 9 | ] 10 | }, 11 | "jiraIssue": "", 12 | "productOwner": "kschlangenheldt", 13 | "teamsChannelURL": "https://teams.microsoft.com/l/channel/somechannel", 14 | "timeStamp": "2022-11-06T18:14:10Z" 15 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/owner.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 3 | "contact": "somebody@some-organisation.com", 4 | "defaultJiraProject": "ISSUE", 5 | "groups": { 6 | "users": [ 7 | "some-other-user", 8 | "a-very-special-user" 9 | ] 10 | }, 11 | "jiraIssue": "ISSUE-0000", 12 | "productOwner": "kschlangenheldt", 13 | "teamsChannelURL": "https://teams.microsoft.com/l/channel/somechannel", 14 | "timeStamp": "2022-11-06T18:14:10Z" 15 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/owner-patch.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a2430000000000", 3 | "contact": "changed@some-organisation.com", 4 | "defaultJiraProject": "ISSUE", 5 | "groups": { 6 | "users": [ 7 | "some-other-user", 8 | "a-very-special-user" 9 | ] 10 | }, 11 | "jiraIssue": "ISSUE-2345", 12 | "productOwner": "kschlangenheldt", 13 | "teamsChannelURL": "https://teams.microsoft.com/l/channel/somechannel", 14 | "timeStamp": "2022-11-06T18:14:10Z" 15 | } -------------------------------------------------------------------------------- /internal/acorn/application/appint.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | // ApplicationName is used as a default for logging very early errors when the configuration isn't read yet. 4 | const ApplicationName = "metadata" 5 | 6 | // Application is the central singleton representing the entire application. 7 | type Application interface { 8 | IsApplication() bool 9 | 10 | // Run runs the application, including setup and teardown phase 11 | // 12 | // returns the exit code - we do not call os.Exit inside 13 | Run() int 14 | } 15 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/owner-create-duplicate.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 3 | "contact": "somebody@some-organisation.com", 4 | "defaultJiraProject": "ISSUE", 5 | "groups": { 6 | "users": [ 7 | "some-other-user", 8 | "a-very-special-user" 9 | ] 10 | }, 11 | "jiraIssue": "ISSUE-0000", 12 | "productOwner": "kschlangenheldt", 13 | "teamsChannelURL": "https://teams.microsoft.com/l/channel/somechannel", 14 | "timeStamp": "2022-11-06T18:14:10Z" 15 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/owner-patch-conflict.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 3 | "contact": "somebody@some-organisation.com", 4 | "defaultJiraProject": "ISSUE", 5 | "groups": { 6 | "users": [ 7 | "some-other-user", 8 | "a-very-special-user" 9 | ] 10 | }, 11 | "jiraIssue": "ISSUE-0000", 12 | "productOwner": "kschlangenheldt", 13 | "teamsChannelURL": "https://teams.microsoft.com/l/channel/somechannel", 14 | "timeStamp": "2022-11-06T18:14:10Z" 15 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/owner-update-conflict.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 3 | "contact": "somebody@some-organisation.com", 4 | "defaultJiraProject": "ISSUE", 5 | "groups": { 6 | "users": [ 7 | "some-other-user", 8 | "a-very-special-user" 9 | ] 10 | }, 11 | "jiraIssue": "ISSUE-0000", 12 | "productOwner": "kschlangenheldt", 13 | "teamsChannelURL": "https://teams.microsoft.com/l/channel/somechannel", 14 | "timeStamp": "2022-11-06T18:14:10Z" 15 | } -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # Make sure to check the documentation at https://goreleaser.com 2 | before: 3 | hooks: 4 | - go mod tidy 5 | builds: 6 | - env: 7 | - CGO_ENABLED=0 8 | goos: 9 | - linux 10 | - windows 11 | - darwin 12 | goarch: 13 | - amd64 14 | archives: 15 | - format: binary 16 | checksum: 17 | name_template: 'checksums.txt' 18 | snapshot: 19 | name_template: "{{ incpatch .Version }}-next" 20 | changelog: 21 | sort: asc 22 | filters: 23 | exclude: 24 | - '^test:' 25 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-unchanged.json: -------------------------------------------------------------------------------- 1 | { 2 | "alertTarget": "https://webhook.com/9asdflk29d4m39g", 3 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 4 | "jiraIssue": "", 5 | "owner": "some-owner", 6 | "quicklinks": [ 7 | { 8 | "title": "Swagger UI", 9 | "url": "/swagger-ui/index.html" 10 | } 11 | ], 12 | "repositories": [ 13 | "some-service-backend.helm-deployment", 14 | "some-service-backend.implementation" 15 | ], 16 | "timeStamp": "2022-11-06T18:14:10Z" 17 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service.json: -------------------------------------------------------------------------------- 1 | { 2 | "alertTarget": "https://webhook.com/9asdflk29d4m39g", 3 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 4 | "jiraIssue": "ISSUE-0000", 5 | "owner": "some-owner", 6 | "quicklinks": [ 7 | { 8 | "title": "Swagger UI", 9 | "url": "/swagger-ui/index.html" 10 | } 11 | ], 12 | "repositories": [ 13 | "some-service-backend.helm-deployment", 14 | "some-service-backend.implementation" 15 | ], 16 | "timeStamp": "2022-11-06T18:14:10Z" 17 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-original.json: -------------------------------------------------------------------------------- 1 | { 2 | "alertTarget": "https://webhook.com/9asdflk29d4m39g", 3 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 4 | "jiraIssue": "ISSUE-0000", 5 | "owner": "some-owner", 6 | "quicklinks": [ 7 | { 8 | "title": "Swagger UI", 9 | "url": "/swagger-ui/index.html" 10 | } 11 | ], 12 | "repositories": [ 13 | "some-service-backend.helm-deployment", 14 | "some-service-backend.implementation" 15 | ], 16 | "timeStamp": "2022-11-06T18:14:10Z" 17 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-patch-conflict.json: -------------------------------------------------------------------------------- 1 | { 2 | "alertTarget": "https://webhook.com/9asdflk29d4m39g", 3 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 4 | "jiraIssue": "ISSUE-0000", 5 | "owner": "some-owner", 6 | "quicklinks": [ 7 | { 8 | "title": "Swagger UI", 9 | "url": "/swagger-ui/index.html" 10 | } 11 | ], 12 | "repositories": [ 13 | "some-service-backend.helm-deployment", 14 | "some-service-backend.implementation" 15 | ], 16 | "timeStamp": "2022-11-06T18:14:10Z" 17 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-update-conflict.json: -------------------------------------------------------------------------------- 1 | { 2 | "alertTarget": "https://webhook.com/9asdflk29d4m39g", 3 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 4 | "jiraIssue": "ISSUE-0000", 5 | "owner": "some-owner", 6 | "quicklinks": [ 7 | { 8 | "title": "Swagger UI", 9 | "url": "/swagger-ui/index.html" 10 | } 11 | ], 12 | "repositories": [ 13 | "some-service-backend.helm-deployment", 14 | "some-service-backend.implementation" 15 | ], 16 | "timeStamp": "2022-11-06T18:14:10Z" 17 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-create-duplicate.json: -------------------------------------------------------------------------------- 1 | { 2 | "alertTarget": "https://webhook.com/9asdflk29d4m39g", 3 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 4 | "jiraIssue": "ISSUE-0000", 5 | "owner": "some-owner", 6 | "quicklinks": [ 7 | { 8 | "title": "Swagger UI", 9 | "url": "/swagger-ui/index.html" 10 | } 11 | ], 12 | "repositories": [ 13 | "some-service-backend.helm-deployment", 14 | "some-service-backend.implementation" 15 | ], 16 | "timeStamp": "2022-11-06T18:14:10Z" 17 | } -------------------------------------------------------------------------------- /api/generated_model_merge_strategy.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // MergeStrategy struct for MergeStrategy 15 | type MergeStrategy struct { 16 | Id string `yaml:"id" json:"id"` 17 | } 18 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-create.json: -------------------------------------------------------------------------------- 1 | { 2 | "alertTarget": "squad_nothing@some-organisation.com", 3 | "commitHash": "6c8ac2c35791edf9979623c717a2430000000000", 4 | "internetExposed": true, 5 | "jiraIssue": "ISSUE-2345", 6 | "lifecycle": "experimental", 7 | "owner": "some-owner", 8 | "quicklinks": [ 9 | { 10 | "title": "Swagger UI", 11 | "url": "/swagger-ui/index.html" 12 | } 13 | ], 14 | "repositories": [ 15 | "whatever.helm-deployment", 16 | "whatever.implementation" 17 | ], 18 | "timeStamp": "2022-11-06T18:14:10Z" 19 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-create-crossref.json: -------------------------------------------------------------------------------- 1 | { 2 | "alertTarget": "squad_nothing@some-organisation.com", 3 | "commitHash": "6c8ac2c35791edf9979623c717a2430000000000", 4 | "internetExposed": true, 5 | "jiraIssue": "ISSUE-2345", 6 | "lifecycle": "experimental", 7 | "owner": "some-owner", 8 | "quicklinks": [ 9 | { 10 | "title": "Swagger UI", 11 | "url": "/swagger-ui/index.html" 12 | } 13 | ], 14 | "repositories": [ 15 | "crossref.helm-deployment", 16 | "not-crossref.implementation" 17 | ], 18 | "timeStamp": "2022-11-06T18:14:10Z" 19 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-patch-crossref.json: -------------------------------------------------------------------------------- 1 | { 2 | "alertTarget": "squad_nothing@some-organisation.com", 3 | "commitHash": "6c8ac2c35791edf9979623c717a2430000000000", 4 | "internetExposed": true, 5 | "jiraIssue": "ISSUE-2345", 6 | "lifecycle": "experimental", 7 | "owner": "some-owner", 8 | "quicklinks": [ 9 | { 10 | "title": "Swagger UI", 11 | "url": "/swagger-ui/index.html" 12 | } 13 | ], 14 | "repositories": [ 15 | "crossref.helm-deployment", 16 | "not-crossref.implementation" 17 | ], 18 | "timeStamp": "2022-11-06T18:14:10Z" 19 | } -------------------------------------------------------------------------------- /api/generated_model_post_promote.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // PostPromote struct for PostPromote 15 | type PostPromote struct { 16 | Binaries []Binary `yaml:"binaries,omitempty" json:"binaries,omitempty"` 17 | } 18 | -------------------------------------------------------------------------------- /test/mock/authprovidermock/authprovidermock.go: -------------------------------------------------------------------------------- 1 | package authprovidermock 2 | 3 | import ( 4 | "context" 5 | "github.com/go-git/go-git/v5/plumbing/transport" 6 | ) 7 | 8 | type AuthProviderMock struct { 9 | } 10 | 11 | func (this *AuthProviderMock) IsAuthProvider() bool { 12 | return true 13 | } 14 | 15 | func (this *AuthProviderMock) Setup() error { 16 | return nil 17 | } 18 | 19 | func (this *AuthProviderMock) SetupProvider(ctx context.Context) error { 20 | return nil 21 | } 22 | 23 | func (this *AuthProviderMock) ProvideAuth(_ context.Context) transport.AuthMethod { 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-patch-crossref-ownerchange.json: -------------------------------------------------------------------------------- 1 | { 2 | "alertTarget": "squad_nothing@some-organisation.com", 3 | "commitHash": "6c8ac2c35791edf9979623c717a2430000000000", 4 | "internetExposed": true, 5 | "jiraIssue": "ISSUE-2345", 6 | "lifecycle": "experimental", 7 | "owner": "deleteme", 8 | "quicklinks": [ 9 | { 10 | "title": "Swagger UI", 11 | "url": "/swagger-ui/index.html" 12 | } 13 | ], 14 | "repositories": [ 15 | "crossref.helm-deployment", 16 | "not-crossref.implementation" 17 | ], 18 | "timeStamp": "2022-11-06T18:14:10Z" 19 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-patch.json: -------------------------------------------------------------------------------- 1 | { 2 | "alertTarget": "squad_nothing@some-organisation.com", 3 | "commitHash": "6c8ac2c35791edf9979623c717a2430000000000", 4 | "internetExposed": true, 5 | "jiraIssue": "ISSUE-2345", 6 | "lifecycle": "experimental", 7 | "owner": "some-owner", 8 | "quicklinks": [ 9 | { 10 | "title": "Swagger UI", 11 | "url": "/swagger-ui/index.html" 12 | } 13 | ], 14 | "repositories": [ 15 | "some-service-backend.helm-deployment", 16 | "some-service-backend.implementation" 17 | ], 18 | "timeStamp": "2022-11-06T18:14:10Z" 19 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "alertTarget": "squad_nothing@some-organisation.com", 3 | "commitHash": "6c8ac2c35791edf9979623c717a2430000000000", 4 | "internetExposed": true, 5 | "jiraIssue": "ISSUE-2345", 6 | "lifecycle": "experimental", 7 | "owner": "some-owner", 8 | "quicklinks": [ 9 | { 10 | "title": "Swagger UI", 11 | "url": "/swagger-ui/index.html" 12 | } 13 | ], 14 | "repositories": [ 15 | "some-service-backend.helm-deployment", 16 | "some-service-backend.implementation" 17 | ], 18 | "timeStamp": "2022-11-06T18:14:10Z" 19 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-patch-newowner.json: -------------------------------------------------------------------------------- 1 | { 2 | "alertTarget": "squad_nothing@some-organisation.com", 3 | "commitHash": "6c8ac2c35791edf9979623c717a2430000000000", 4 | "internetExposed": true, 5 | "jiraIssue": "ISSUE-2345", 6 | "lifecycle": "experimental", 7 | "owner": "deleteme", 8 | "quicklinks": [ 9 | { 10 | "title": "Swagger UI", 11 | "url": "/swagger-ui/index.html" 12 | } 13 | ], 14 | "repositories": [ 15 | "some-service-backend.helm-deployment", 16 | "some-service-backend.implementation" 17 | ], 18 | "timeStamp": "2022-11-06T18:14:10Z" 19 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-update-newowner.json: -------------------------------------------------------------------------------- 1 | { 2 | "alertTarget": "squad_nothing@some-organisation.com", 3 | "commitHash": "6c8ac2c35791edf9979623c717a2430000000000", 4 | "internetExposed": true, 5 | "jiraIssue": "ISSUE-2345", 6 | "lifecycle": "experimental", 7 | "owner": "deleteme", 8 | "quicklinks": [ 9 | { 10 | "title": "Swagger UI", 11 | "url": "/swagger-ui/index.html" 12 | } 13 | ], 14 | "repositories": [ 15 | "some-service-backend.helm-deployment", 16 | "some-service-backend.implementation" 17 | ], 18 | "timeStamp": "2022-11-06T18:14:10Z" 19 | } -------------------------------------------------------------------------------- /api/generated_model_link.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // Link A link 15 | type Link struct { 16 | Url *string `yaml:"url,omitempty" json:"url,omitempty"` 17 | Title *string `yaml:"title,omitempty" json:"title,omitempty"` 18 | } 19 | -------------------------------------------------------------------------------- /api/generated_model_service_promoters_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // ServicePromotersDto struct for ServicePromotersDto 15 | type ServicePromotersDto struct { 16 | Promoters []string `yaml:"promoters" json:"promoters"` 17 | } 18 | -------------------------------------------------------------------------------- /api/generated_model_deletion_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // DeletionDto struct for DeletionDto 15 | type DeletionDto struct { 16 | // The jira issue to use for committing the deletion. 17 | JiraIssue string `yaml:"-" json:"jiraIssue"` 18 | } 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GOLANG_VERSION=1 2 | 3 | FROM golang:${GOLANG_VERSION} AS build 4 | 5 | COPY . /app 6 | WORKDIR /app 7 | 8 | RUN go install github.com/jstemmer/go-junit-report/v2@v2.0.0-beta1 9 | 10 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" main.go \ 11 | && go test -v ./... -coverpkg=./internal/... 2>&1 | go-junit-report -set-exit-code -iocopy -out report.xml \ 12 | && go vet ./... 13 | 14 | FROM scratch 15 | 16 | COPY --from=build /app/main /main 17 | COPY --from=build /etc/ssl/certs /etc/ssl/certs 18 | COPY --from=build /app/api/openapi-v3-spec.yaml /api/openapi-v3-spec.yaml 19 | 20 | ENTRYPOINT ["/main"] 21 | -------------------------------------------------------------------------------- /internal/service/mapper/jiraissue_test.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | "github.com/stretchr/testify/require" 5 | "testing" 6 | ) 7 | 8 | func TestJiraIssue_EmptyCommitMessage(t *testing.T) { 9 | result := jiraIssue("") 10 | require.Equal(t, "", result) 11 | } 12 | 13 | func TestJiraIssue_DefaultCommitMessageStyle(t *testing.T) { 14 | result := jiraIssue("ISSUE-000: some commit message text") 15 | require.Equal(t, "ISSUE-000", result) 16 | } 17 | 18 | func TestJiraIssue_MergeCommitMessage(t *testing.T) { 19 | result := jiraIssue("Pull request #23: ISSUE-000: some commit message text") 20 | require.Equal(t, "ISSUE-000", result) 21 | } 22 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-patch.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a2430000000000", 3 | "configuration": { 4 | "branchNameRegex": "testing_.*", 5 | "refProtections": { 6 | "branches": { 7 | "requirePR": [ 8 | { 9 | "pattern": ".*" 10 | } 11 | ] 12 | } 13 | }, 14 | "requireIssue": true 15 | }, 16 | "jiraIssue": "ISSUE-2345", 17 | "mainline": "main", 18 | "owner": "some-owner", 19 | "timeStamp": "2022-11-06T18:14:10Z", 20 | "type": "helm-chart", 21 | "url": "ssh://git@bitbucket.some-organisation.com:7999/helm/karma-wrapper.git" 22 | } -------------------------------------------------------------------------------- /test/acceptance/util_main_test.go: -------------------------------------------------------------------------------- 1 | package acceptance 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | // package global test entry point (the Main() that runs only once and runs all the tests in this package) 9 | // 10 | // Note how we only start up the service once, then run all the acceptance tests. This is much faster 11 | // than doing this every time. 12 | 13 | func TestMain(m *testing.M) { 14 | err := tstSetup(validConfigurationPath) 15 | if err != nil { 16 | println("error during global acceptance test initialization - BAILING OUT!") 17 | os.Exit(1) 18 | } 19 | defer tstShutdown() 20 | 21 | code := m.Run() 22 | os.Exit(code) 23 | } 24 | -------------------------------------------------------------------------------- /api/generated_model_exclude_merge_check_user_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // ExcludeMergeCheckUserDto struct for ExcludeMergeCheckUserDto 15 | type ExcludeMergeCheckUserDto struct { 16 | // Name of merge check exclude user 17 | Name string `yaml:"name" json:"name"` 18 | } 19 | -------------------------------------------------------------------------------- /api/generated_model_repository_configuration_default_task_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // RepositoryConfigurationDefaultTaskDto struct for RepositoryConfigurationDefaultTaskDto 15 | type RepositoryConfigurationDefaultTaskDto struct { 16 | Text string `yaml:"text" json:"text"` 17 | } 18 | -------------------------------------------------------------------------------- /internal/acorn/repository/notifierint.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | openapi "github.com/Interhyp/metadata-service/api" 6 | "github.com/Interhyp/metadata-service/internal/types" 7 | ) 8 | 9 | type Notifier interface { 10 | IsNotifier() bool 11 | 12 | Setup() error 13 | 14 | SetupNotifier(ctx context.Context) error 15 | 16 | PublishCreation(ctx context.Context, payloadName string, payload openapi.NotificationPayload) error 17 | 18 | PublishModification(ctx context.Context, payloadName string, payload openapi.NotificationPayload) error 19 | 20 | PublishDeletion(ctx context.Context, payloadName string, payloadType types.NotificationPayloadType) 21 | } 22 | -------------------------------------------------------------------------------- /api/generated_model_health_component.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // HealthComponent struct for HealthComponent 15 | type HealthComponent struct { 16 | Description *string `yaml:"description,omitempty" json:"description,omitempty"` 17 | Status *string `yaml:"status,omitempty" json:"status,omitempty"` 18 | } 19 | -------------------------------------------------------------------------------- /test/mock/vaultmock/vault.go: -------------------------------------------------------------------------------- 1 | package vaultmock 2 | 3 | import ( 4 | "context" 5 | librepo "github.com/Interhyp/go-backend-service-common/acorns/repository" 6 | ) 7 | 8 | type VaultImpl struct { 9 | } 10 | 11 | func New() librepo.Vault { 12 | return &VaultImpl{} 13 | } 14 | 15 | func (v *VaultImpl) IsVault() bool { 16 | return true 17 | } 18 | 19 | func (v *VaultImpl) Execute() error { 20 | return nil 21 | } 22 | 23 | func (v *VaultImpl) Setup(ctx context.Context) error { 24 | return nil 25 | } 26 | 27 | func (v *VaultImpl) Authenticate(ctx context.Context) error { 28 | return nil 29 | } 30 | 31 | func (v *VaultImpl) ObtainSecrets(ctx context.Context) error { 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /api/generated_model_ref_protections.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // RefProtections Configures available protections for git refs 15 | type RefProtections struct { 16 | Branches *RefProtectionsBranches `yaml:"branches,omitempty" json:"branches,omitempty"` 17 | Tags *RefProtectionsTags `yaml:"tags,omitempty" json:"tags,omitempty"` 18 | } 19 | -------------------------------------------------------------------------------- /api/generated_model_owner_list_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // OwnerListDto struct for OwnerListDto 15 | type OwnerListDto struct { 16 | Owners map[string]OwnerDto `yaml:"owners" json:"owners"` 17 | // ISO-8601 UTC date time at which the list of owners was obtained from service-metadata 18 | TimeStamp string `yaml:"-" json:"timeStamp"` 19 | } 20 | -------------------------------------------------------------------------------- /api/generated_model_quicklink.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // Quicklink struct for Quicklink 15 | type Quicklink struct { 16 | Url *string `yaml:"url,omitempty" json:"url,omitempty"` 17 | Title *string `yaml:"title,omitempty" json:"title,omitempty"` 18 | Description *string `yaml:"description,omitempty" json:"description,omitempty"` 19 | } 20 | -------------------------------------------------------------------------------- /api/generated_model_service_list_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // ServiceListDto struct for ServiceListDto 15 | type ServiceListDto struct { 16 | Services map[string]ServiceDto `yaml:"services" json:"services"` 17 | // ISO-8601 UTC date time at which the list of services was obtained from service-metadata 18 | TimeStamp string `yaml:"-" json:"timeStamp"` 19 | } 20 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | # Checklist 8 | 9 | - [ ] I have read the [CONTRIBUTING.md](/CONTRIBUTING-template.md) 10 | - [ ] I have made corresponding changes to the documentation 11 | - [ ] My changes generate no lint errors 12 | - [ ] I have added tests that prove my fix is effective or that my feature works 13 | - [ ] New and existing tests pass locally with my changes 14 | - [ ] Only MIT licensed or MIT license compatible dependencies are used (e.g.: Apache2 or BSD) 15 | - [ ] The code contains no credentials, personalized data or company secrets -------------------------------------------------------------------------------- /api/generated_model_repository_list_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // RepositoryListDto struct for RepositoryListDto 15 | type RepositoryListDto struct { 16 | Repositories map[string]RepositoryDto `yaml:"repositories" json:"repositories"` 17 | // ISO-8601 UTC date time at which the list of repositories was obtained from service-metadata 18 | TimeStamp string `yaml:"-" json:"timeStamp"` 19 | } 20 | -------------------------------------------------------------------------------- /internal/service/mapper/jiraissue.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | const mergeMsgRegex = "Pull request #[0-9]+\\: " 9 | 10 | func jiraIssue(commitMessage string) string { 11 | regex, _ := regexp.Compile(mergeMsgRegex) 12 | commitMessage = regex.ReplaceAllString(commitMessage, "") 13 | 14 | fields := strings.FieldsFunc(commitMessage, func(r rune) bool { 15 | // split at anything that is not A-Z 0-9 - 16 | if 'A' <= r && r <= 'Z' { 17 | return false 18 | } else if '0' <= r && r <= '9' { 19 | return false 20 | } else if '-' == r { 21 | return false 22 | } else { 23 | return true 24 | } 25 | }) 26 | if len(fields) > 0 && len(fields[0]) > 1 { 27 | return fields[0] 28 | } else { 29 | return "" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/service-patch-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "alertTarget": "squad_nothing@some-organisation.com", 3 | "commitHash": "6c8ac2c35791edf9979623c717a2430000000000", 4 | "internetExposed": true, 5 | "jiraIssue": "ISSUE-2345", 6 | "lifecycle": "experimental", 7 | "owner": "some-owner", 8 | "quicklinks": [ 9 | { 10 | "title": "Swagger UI", 11 | "url": "/swagger-ui/index.html" 12 | } 13 | ], 14 | "repositories": [ 15 | "some-service-backend.helm-deployment", 16 | "some-service-backend.implementation" 17 | ], 18 | "spec": { 19 | "consumesApis": [ 20 | "some-api" 21 | ], 22 | "dependsOn": [ 23 | "some-service", 24 | "other-service" 25 | ] 26 | }, 27 | "timeStamp": "2022-11-06T18:14:10Z" 28 | } -------------------------------------------------------------------------------- /api/generated_model_error_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | import ( 15 | "time" 16 | ) 17 | 18 | // ErrorDto struct for ErrorDto 19 | type ErrorDto struct { 20 | Details *string `yaml:"details,omitempty" json:"details,omitempty"` 21 | Message *string `yaml:"message,omitempty" json:"message,omitempty"` 22 | Timestamp *time.Time `yaml:"timestamp,omitempty" json:"timestamp,omitempty"` 23 | } 24 | -------------------------------------------------------------------------------- /api/generated_model_notification_payload.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // NotificationPayload struct for NotificationPayload 15 | type NotificationPayload struct { 16 | Owner *OwnerDto `yaml:"Owner,omitempty" json:"Owner,omitempty"` 17 | Service *ServiceDto `yaml:"Service,omitempty" json:"Service,omitempty"` 18 | Repository *RepositoryDto `yaml:"Repository,omitempty" json:"Repository,omitempty"` 19 | } 20 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | 2 | # To get started with Dependabot version updates, you'll need to specify which 3 | # package ecosystems to update and where the package manifests are located. 4 | # Please see the documentation for all configuration options: 5 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 6 | 7 | version: 2 8 | updates: 9 | - package-ecosystem: "gomod" # See documentation for possible values 10 | directory: "/" # Location of package manifests 11 | schedule: 12 | interval: "daily" 13 | labels: 14 | - "dependencies" 15 | reviewers: 16 | - "Interhyp/technical-excellence" 17 | commit-message: 18 | prefix: "fix" 19 | include: "scope" 20 | pull-request-branch-name: 21 | separator: "-" 22 | -------------------------------------------------------------------------------- /.github/workflows/semantic-commit-message-check.yaml: -------------------------------------------------------------------------------- 1 | name: 'Semantic Commit Message Checker' 2 | on: 3 | push: 4 | branches-ignore: 5 | - main 6 | 7 | jobs: 8 | check-commit-message: 9 | name: Check Commit Message 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check valid types 13 | uses: gsactions/commit-message-checker@v1 14 | with: 15 | pattern: '^(fix|feat|docs|style|perf|refactor|test|build|chore|ci|revert)(\([\w_-]+\))?!?: .*' 16 | error: 'Your commit message should match one of these types (build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test) in header.' 17 | excludeDescription: 'true' 18 | excludeTitle: 'true' 19 | checkAllCommitMessages: 'true' 20 | accessToken: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /api/generated_model_repository_configuration_dto_merge_config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // RepositoryConfigurationDtoMergeConfig struct for RepositoryConfigurationDtoMergeConfig 15 | type RepositoryConfigurationDtoMergeConfig struct { 16 | DefaultStrategy *MergeStrategy `yaml:"defaultStrategy,omitempty" json:"defaultStrategy,omitempty"` 17 | Strategies []MergeStrategy `yaml:"strategies,omitempty" json:"strategies,omitempty"` 18 | } 19 | -------------------------------------------------------------------------------- /api/generated_model_pull_requests.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // PullRequests Configures pull request settings 15 | type PullRequests struct { 16 | // Allows merge commits on pull requests 17 | AllowMergeCommits *bool `yaml:"allowMergeCommits,omitempty" json:"allowMergeCommits,omitempty"` 18 | // Allows rebase merging on pull requests 19 | AllowRebaseMerging *bool `yaml:"allowRebaseMerging,omitempty" json:"allowRebaseMerging,omitempty"` 20 | } 21 | -------------------------------------------------------------------------------- /api/generated_model_repository_configuration_access_key_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // RepositoryConfigurationAccessKeyDto struct for RepositoryConfigurationAccessKeyDto 15 | type RepositoryConfigurationAccessKeyDto struct { 16 | Key *string `yaml:"key,omitempty" json:"key,omitempty"` 17 | Data *string `yaml:"data,omitempty" json:"data,omitempty"` 18 | Permission *string `yaml:"permission,omitempty" json:"permission,omitempty"` 19 | } 20 | -------------------------------------------------------------------------------- /internal/service/updater/metadata.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/Interhyp/metadata-service/internal/acorn/repository" 7 | ) 8 | 9 | func (s *Impl) updateMetadata(ctx context.Context) ([]repository.UpdateEvent, error) { 10 | s.Logging.Logger().Ctx(ctx).Info().Print("refreshing metadata") 11 | 12 | events, err := s.Mapper.RefreshMetadata(ctx) 13 | if err != nil { 14 | s.totalErrorCounter.Inc() 15 | s.metadataErrorCounter.Inc() 16 | return events, err 17 | } 18 | 19 | if err := ctx.Err(); err != nil { 20 | if errors.Is(err, context.Canceled) { 21 | s.Logging.Logger().Ctx(ctx).Warn().Print("timeout while updating metadata repository clone") 22 | s.totalErrorCounter.Inc() 23 | s.metadataErrorCounter.Inc() 24 | return events, err 25 | } 26 | } 27 | 28 | return events, nil 29 | } 30 | -------------------------------------------------------------------------------- /pkg/recorder/recorder.go: -------------------------------------------------------------------------------- 1 | package recorder 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "fmt" 7 | "net/url" 8 | "strings" 9 | ) 10 | 11 | func ConstructFilenameV4(method string, requestUrl string, _ interface{}) (string, error) { 12 | parsedUrl, err := url.Parse(requestUrl) 13 | if err != nil { 14 | return "", err 15 | } 16 | 17 | m := strings.ToLower(method) 18 | md5sumOverPath := md5.Sum([]byte(parsedUrl.EscapedPath())) 19 | p := hex.EncodeToString(md5sumOverPath[:]) 20 | p = p[:8] 21 | // we have to ensure the filenames don't get too long. git for windows only supports 260 character paths 22 | md5sumOverQuery := md5.Sum([]byte(parsedUrl.Query().Encode())) 23 | q := hex.EncodeToString(md5sumOverQuery[:]) 24 | q = q[:8] 25 | 26 | filename := fmt.Sprintf("request_%s_%s_%s.json", m, p, q) 27 | return filename, nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/service/util/UserGroupHelper_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/stretchr/testify/require" 5 | "testing" 6 | ) 7 | 8 | func TestParseGroupOwnerAndGroupName(t *testing.T) { 9 | 10 | isGroup, ownerOfGroup, nameOfGroup := ParseGroupOwnerAndGroupName("@someOwner.someGroupName") 11 | require.True(t, isGroup) 12 | require.Equal(t, "someOwner", ownerOfGroup) 13 | require.Equal(t, "someGroupName", nameOfGroup) 14 | 15 | isGroup, ownerOfGroup, nameOfGroup = ParseGroupOwnerAndGroupName("someOwner.someGroupName") 16 | require.False(t, isGroup) 17 | require.Equal(t, "", ownerOfGroup) 18 | require.Equal(t, "", nameOfGroup) 19 | 20 | isGroup, ownerOfGroup, nameOfGroup = ParseGroupOwnerAndGroupName("@someGroupName") 21 | require.False(t, isGroup) 22 | require.Equal(t, "", ownerOfGroup) 23 | require.Equal(t, "", nameOfGroup) 24 | } 25 | -------------------------------------------------------------------------------- /test/acceptance/util_notifier_test.go: -------------------------------------------------------------------------------- 1 | package acceptance 2 | 3 | import ( 4 | "github.com/Interhyp/metadata-service/api" 5 | "github.com/Interhyp/metadata-service/internal/types" 6 | "github.com/Interhyp/metadata-service/test/mock/notifiermock" 7 | "github.com/stretchr/testify/require" 8 | "testing" 9 | ) 10 | 11 | func hasSentNotification(t *testing.T, clientIdentifier string, name string, event types.NotificationEventType, payloadType types.NotificationPayloadType, payload *openapi.NotificationPayload) { 12 | client := notifierImpl.Clients[clientIdentifier] 13 | mockClient := client.(*notifiermock.NotifierClientMock) 14 | expected := openapi.Notification{ 15 | Name: name, 16 | Event: event.String(), 17 | Type: payloadType.String(), 18 | Payload: payload, 19 | } 20 | require.Contains(t, mockClient.SentNotifications, mockClient.ToJson(expected)) 21 | } 22 | -------------------------------------------------------------------------------- /api/generated_model_repository_configuration_webhooks_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // RepositoryConfigurationWebhooksDto Webhooks configured to the repository. 15 | type RepositoryConfigurationWebhooksDto struct { 16 | // List of predefined webhooks 17 | Predefined []string `yaml:"predefined,omitempty" json:"predefined,omitempty"` 18 | // Additional webhooks to be configured. 19 | Additional []RepositoryConfigurationWebhookDto `yaml:"additional,omitempty" json:"additional,omitempty"` 20 | } 21 | -------------------------------------------------------------------------------- /api/generated_model_notification.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // Notification Schema of the Dto sent to all configured downstreams upon a change of service. 15 | type Notification struct { 16 | // name of the service that was updated 17 | Name string `yaml:"name" json:"name"` 18 | Event string `yaml:"event" json:"event"` 19 | Type string `yaml:"type" json:"type"` 20 | Payload *NotificationPayload `yaml:"payload,omitempty" json:"payload,omitempty"` 21 | } 22 | -------------------------------------------------------------------------------- /internal/types/types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | const ( 4 | OwnerPayload NotificationPayloadType = iota 5 | ServicePayload 6 | RepositoryPayload 7 | ) 8 | 9 | func (p NotificationPayloadType) String() string { 10 | switch p { 11 | case OwnerPayload: 12 | return "Owner" 13 | case ServicePayload: 14 | return "Service" 15 | case RepositoryPayload: 16 | return "Repository" 17 | default: 18 | return "" 19 | } 20 | } 21 | 22 | const ( 23 | CreatedEvent NotificationEventType = iota 24 | ModifiedEvent 25 | DeletedEvent 26 | ) 27 | 28 | func (p NotificationEventType) String() string { 29 | switch p { 30 | case CreatedEvent: 31 | return "CREATED" 32 | case ModifiedEvent: 33 | return "MODIFIED" 34 | case DeletedEvent: 35 | return "DELETED" 36 | default: 37 | return "" 38 | } 39 | } 40 | 41 | type NotificationPayloadType uint32 42 | 43 | type NotificationEventType uint32 44 | -------------------------------------------------------------------------------- /api/generated_model_repository_configuration_webhook_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // RepositoryConfigurationWebhookDto struct for RepositoryConfigurationWebhookDto 15 | type RepositoryConfigurationWebhookDto struct { 16 | Name string `yaml:"name" json:"name"` 17 | Url string `yaml:"url" json:"url"` 18 | // Events the webhook should be triggered with. 19 | Events []string `yaml:"events,omitempty" json:"events,omitempty"` 20 | Configuration map[string]string `yaml:"configuration,omitempty" json:"configuration,omitempty"` 21 | } 22 | -------------------------------------------------------------------------------- /test/mock/notifiermock/notifierclientmock.go: -------------------------------------------------------------------------------- 1 | package notifiermock 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | openapi "github.com/Interhyp/metadata-service/api" 8 | ) 9 | 10 | type NotifierClientMock struct { 11 | SentNotifications []string 12 | } 13 | 14 | func (n *NotifierClientMock) Setup(clientIdentifier string, url string) error { 15 | return nil 16 | } 17 | 18 | func (n *NotifierClientMock) Send(ctx context.Context, notification openapi.Notification) { 19 | n.SentNotifications = append(n.SentNotifications, n.ToJson(notification)) 20 | } 21 | 22 | func (n *NotifierClientMock) Reset() { 23 | n.SentNotifications = make([]string, 0) 24 | } 25 | 26 | func (n *NotifierClientMock) ToJson(notification openapi.Notification) string { 27 | notificationJson, err := json.Marshal(¬ification) 28 | if err != nil { 29 | notificationJson = []byte(fmt.Sprintf("error: %s", err.Error())) 30 | } 31 | return string(notificationJson) 32 | } 33 | -------------------------------------------------------------------------------- /internal/acorn/errors/nochangeserror/error.go: -------------------------------------------------------------------------------- 1 | package nochangeserror 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | ) 7 | 8 | // NoChangesError is raised when an empty commit occurs. 9 | type NoChangesError interface { 10 | Ctx() context.Context 11 | IsNoChanges() bool 12 | } 13 | 14 | // this also implements the error interface 15 | 16 | type Impl struct { 17 | ctx context.Context 18 | err error 19 | } 20 | 21 | func New(ctx context.Context) error { 22 | return &Impl{ 23 | ctx: ctx, 24 | err: errors.New("empty commit"), 25 | } 26 | } 27 | 28 | func (e *Impl) Error() string { 29 | return e.err.Error() 30 | } 31 | 32 | func (e *Impl) Ctx() context.Context { 33 | return e.ctx 34 | } 35 | 36 | // the presence of this method makes the interface unique and thus recognizable by a simple type check 37 | 38 | func (e *Impl) IsNoChanges() bool { 39 | return true 40 | } 41 | 42 | func Is(err error) bool { 43 | _, ok := err.(NoChangesError) 44 | return ok 45 | } 46 | -------------------------------------------------------------------------------- /internal/acorn/errors/inputvalidationerror/error.go: -------------------------------------------------------------------------------- 1 | package inputvalidationerror 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | type ErrorInt interface { 10 | Ctx() context.Context 11 | IsInputValidationError() bool 12 | } 13 | 14 | // this also implements the error interface 15 | 16 | type Error struct { 17 | ctx context.Context 18 | err error 19 | } 20 | 21 | func New(ctx context.Context, reason string) error { 22 | return &Error{ 23 | ctx: ctx, 24 | err: fmt.Errorf("input validation failed: %s", reason), 25 | } 26 | } 27 | 28 | func (e *Error) Error() string { 29 | return e.err.Error() 30 | } 31 | 32 | func (e *Error) Ctx() context.Context { 33 | return e.ctx 34 | } 35 | 36 | // the presence of this method makes the interface unique and thus recognizable by a simple type check 37 | 38 | func (e *Error) IsInputValidationError() bool { 39 | return true 40 | } 41 | 42 | func Is(err error) bool { 43 | return errors.As(err, new(*Error)) 44 | } 45 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repositories-filtered-type.json: -------------------------------------------------------------------------------- 1 | { 2 | "repositories": { 3 | "some-service-backend.implementation": { 4 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 5 | "generator": "java-spring-cloud", 6 | "jiraIssue": "ISSUE-0000", 7 | "mainline": "master", 8 | "owner": "some-owner", 9 | "timeStamp": "2022-11-06T18:14:10Z", 10 | "type": "implementation", 11 | "url": "ssh://git@bitbucket.some-organisation.com:7999/PROJECT/some-service-backend.git" 12 | }, 13 | "whatever.implementation": { 14 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 15 | "generator": "java-spring-cloud", 16 | "jiraIssue": "ISSUE-0000", 17 | "mainline": "master", 18 | "owner": "some-owner", 19 | "timeStamp": "2022-11-06T18:14:10Z", 20 | "type": "implementation", 21 | "url": "ssh://git@bitbucket.some-organisation.com:7999/PROJECT/whatever.git" 22 | } 23 | }, 24 | "timeStamp": "2022-11-06T18:14:10Z" 25 | } -------------------------------------------------------------------------------- /api/generated_model_binary.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // Binary Parameters to identify a binary in e.g. nexus 15 | type Binary struct { 16 | // The group id of binary 17 | GroupId string `yaml:"groupId" json:"groupId"` 18 | // The artifact id of binary 19 | ArtifactId string `yaml:"artifactId" json:"artifactId"` 20 | // The version prefix of binary 21 | VersionPrefix string `yaml:"versionPrefix" json:"versionPrefix"` 22 | // The classifier of binary 23 | Classifier *string `yaml:"classifier,omitempty" json:"classifier,omitempty"` 24 | // The file type of binary e.g. tar.gz 25 | FileType *string `yaml:"fileType,omitempty" json:"fileType,omitempty"` 26 | } 27 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-create.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a2430000000000", 3 | "configuration": { 4 | "accessKeys": [ 5 | { 6 | "key": "KEY", 7 | "permission": "REPO_WRITE" 8 | } 9 | ], 10 | "approvers": { 11 | "testing": [ 12 | "some-user" 13 | ] 14 | }, 15 | "commitMessageType": "SEMANTIC", 16 | "requireConditions": { 17 | "snyk-key": { 18 | "refMatcher": "master" 19 | } 20 | }, 21 | "requireIssue": false, 22 | "requireSuccessfulBuilds": 1, 23 | "webhooks": { 24 | "additional": [ 25 | { 26 | "events": [ 27 | "event" 28 | ], 29 | "name": "webhookname", 30 | "url": "webhookurl" 31 | } 32 | ] 33 | } 34 | }, 35 | "jiraIssue": "ISSUE-2345", 36 | "mainline": "master", 37 | "owner": "some-owner", 38 | "timeStamp": "2022-11-06T18:14:10Z", 39 | "url": "ssh://git@bitbucket.some-organisation.com:7999/helm/karma-wrapper.git" 40 | } -------------------------------------------------------------------------------- /internal/acorn/repository/idpint.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import "context" 4 | 5 | // IdentityProvider is the central singleton representing an Open ID Connect Identity Provider. 6 | // 7 | // We use this to obtain a JWT keyset and to check its id endpoint to synchronously validate JWT tokens. 8 | type IdentityProvider interface { 9 | IsIdentityProvider() bool 10 | 11 | Setup() error 12 | 13 | // SetupConnector uses the configuration to set up the connector 14 | SetupConnector(ctx context.Context) error 15 | 16 | // ObtainKeySet calls the key set endpoint and converts the keys to PEM for use with the jwt package 17 | ObtainKeySet(ctx context.Context) error 18 | 19 | // GetKeySet returns the previously obtained KeySet 20 | GetKeySet(ctx context.Context) []string 21 | 22 | // VerifyToken ensures synchronously that a token has not been revoked and the account is current. 23 | // 24 | // You should do this for critical operations that cannot live with the usual token 25 | // expiry cycle. 26 | VerifyToken(ctx context.Context, token string) error 27 | } 28 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/owners.json: -------------------------------------------------------------------------------- 1 | { 2 | "owners": { 3 | "deleteme": { 4 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 5 | "contact": "somebody@some-organisation.com", 6 | "defaultJiraProject": "ISSUE", 7 | "jiraIssue": "ISSUE-0000", 8 | "productOwner": "kschlangenheldt", 9 | "teamsChannelURL": "https://teams.microsoft.com/l/channel/somechannel", 10 | "timeStamp": "2022-11-06T18:14:10Z" 11 | }, 12 | "some-owner": { 13 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 14 | "contact": "somebody@some-organisation.com", 15 | "defaultJiraProject": "ISSUE", 16 | "groups": { 17 | "users": [ 18 | "some-other-user", 19 | "a-very-special-user" 20 | ] 21 | }, 22 | "jiraIssue": "ISSUE-0000", 23 | "productOwner": "kschlangenheldt", 24 | "teamsChannelURL": "https://teams.microsoft.com/l/channel/somechannel", 25 | "timeStamp": "2022-11-06T18:14:10Z" 26 | } 27 | }, 28 | "timeStamp": "2022-11-06T18:14:10Z" 29 | } -------------------------------------------------------------------------------- /test/mock/githubmock/githubmock.go: -------------------------------------------------------------------------------- 1 | package githubmock 2 | 3 | import ( 4 | "context" 5 | "github.com/Interhyp/metadata-service/internal/acorn/repository" 6 | "github.com/google/go-github/v70/github" 7 | ) 8 | 9 | type GitHubMock struct{} 10 | 11 | func (this *GitHubMock) StartCheckRun(ctx context.Context, owner, repoName, checkName, sha string) (int64, error) { 12 | return 0, nil 13 | } 14 | 15 | func (this *GitHubMock) ConcludeCheckRun(ctx context.Context, owner, repoName, checkName string, checkRunId int64, conclusion repository.CheckRunConclusion, details github.CheckRunOutput, actions ...*github.CheckRunAction) error { 16 | return nil 17 | } 18 | 19 | func (this *GitHubMock) GetUser(ctx context.Context, username string) (*github.User, error) { 20 | return &github.User{ 21 | Email: github.Ptr("some-email"), 22 | Name: github.Ptr("some-name"), 23 | }, nil 24 | } 25 | 26 | func (this *GitHubMock) CreateInstallationToken(ctx context.Context, installationId int64) (*github.InstallationToken, *github.Response, error) { 27 | return &github.InstallationToken{}, nil, nil 28 | } 29 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a2430000000000", 3 | "configuration": { 4 | "accessKeys": [ 5 | { 6 | "key": "KEY", 7 | "permission": "REPO_WRITE" 8 | } 9 | ], 10 | "approvers": { 11 | "testing": [ 12 | "some-user" 13 | ] 14 | }, 15 | "commitMessageType": "SEMANTIC", 16 | "requireConditions": { 17 | "snyk-key": { 18 | "refMatcher": "master" 19 | } 20 | }, 21 | "requireIssue": false, 22 | "requireSuccessfulBuilds": 1, 23 | "webhooks": { 24 | "additional": [ 25 | { 26 | "events": [ 27 | "event" 28 | ], 29 | "name": "webhookname", 30 | "url": "webhookurl" 31 | } 32 | ] 33 | } 34 | }, 35 | "jiraIssue": "ISSUE-2345", 36 | "mainline": "master", 37 | "owner": "some-owner", 38 | "timeStamp": "2022-11-06T18:14:10Z", 39 | "type": "helm-chart", 40 | "url": "ssh://git@bitbucket.some-organisation.com:7999/helm/karma-wrapper.git" 41 | } -------------------------------------------------------------------------------- /api/generated_model_service_spec_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // ServiceSpecDto struct for ServiceSpecDto 15 | type ServiceSpecDto struct { 16 | // A reference to the system that the component belongs to 17 | System *string `yaml:"system,omitempty" json:"system,omitempty"` 18 | // A relation denoting a dependency on another entity 19 | DependsOn []string `yaml:"dependsOn,omitempty" json:"dependsOn,omitempty"` 20 | // A relation with an API, provided by this entity 21 | ProvidesApis []string `yaml:"providesApis,omitempty" json:"providesApis,omitempty"` 22 | // A relation with an API, consumed by this entity 23 | ConsumesApis []string `yaml:"consumesApis,omitempty" json:"consumesApis,omitempty"` 24 | } 25 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-update-newowner.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a2430000000000", 3 | "configuration": { 4 | "accessKeys": [ 5 | { 6 | "key": "KEY", 7 | "permission": "REPO_WRITE" 8 | } 9 | ], 10 | "approvers": { 11 | "testing": [ 12 | "some-user" 13 | ] 14 | }, 15 | "commitMessageType": "SEMANTIC", 16 | "requireConditions": { 17 | "snyk-key": { 18 | "refMatcher": "master" 19 | } 20 | }, 21 | "requireIssue": false, 22 | "requireSuccessfulBuilds": 1, 23 | "webhooks": { 24 | "additional": [ 25 | { 26 | "events": [ 27 | "event" 28 | ], 29 | "name": "webhookname", 30 | "url": "webhookurl" 31 | } 32 | ] 33 | } 34 | }, 35 | "jiraIssue": "ISSUE-2345", 36 | "mainline": "master", 37 | "owner": "deleteme", 38 | "timeStamp": "2022-11-06T18:14:10Z", 39 | "type": "helm-chart", 40 | "url": "ssh://git@bitbucket.some-organisation.com:7999/helm/karma-wrapper.git" 41 | } -------------------------------------------------------------------------------- /api/generated_model_condition_reference_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // ConditionReferenceDto Configuration of conditional build references. 15 | type ConditionReferenceDto struct { 16 | // Reference of a branch. 17 | RefMatcher string `yaml:"refMatcher" json:"refMatcher"` 18 | // list of users or groups for which this protection does not apply. 19 | Exemptions []string `yaml:"exemptions,omitempty" json:"exemptions,omitempty"` 20 | // The expected source for the required conditional build. 21 | Source *string `yaml:"source,omitempty" json:"source,omitempty"` 22 | // The enforcement level of the ruleset. If not set it is active. 23 | Enforcement *string `yaml:"enforcement,omitempty" json:"enforcement,omitempty"` 24 | } 25 | -------------------------------------------------------------------------------- /internal/repository/cache/ownercache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "github.com/Interhyp/metadata-service/api" 6 | ) 7 | 8 | const ownerWhat = "owner" 9 | 10 | func (s *Impl) SetOwnerListTimestamp(ctx context.Context, timestamp string) error { 11 | return s.setTimestamp(ctx, ownerWhat, ownerTimestampKey, timestamp) 12 | } 13 | 14 | func (s *Impl) GetOwnerListTimestamp(ctx context.Context) (string, error) { 15 | return s.getTimestamp(ctx, ownerWhat, ownerTimestampKey) 16 | } 17 | 18 | func (s *Impl) GetSortedOwnerAliases(ctx context.Context) ([]string, error) { 19 | return getSortedKeys(ctx, ownerWhat, s, s.OwnerCache) 20 | } 21 | 22 | func (s *Impl) GetOwner(ctx context.Context, alias string) (openapi.OwnerDto, error) { 23 | return getEntry(ctx, ownerWhat, s, s.OwnerCache, alias) 24 | } 25 | 26 | func (s *Impl) PutOwner(ctx context.Context, alias string, entry openapi.OwnerDto) error { 27 | return putEntry(ctx, ownerWhat, s, s.OwnerCache, alias, entry) 28 | } 29 | 30 | func (s *Impl) DeleteOwner(ctx context.Context, alias string) error { 31 | return removeEntry(ctx, ownerWhat, s, s.OwnerCache, alias) 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Interhyp AG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /internal/repository/config/accessor_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/stretchr/testify/require" 5 | "testing" 6 | ) 7 | 8 | func TestMetadataRepoName_MetadataRepoUrl(t *testing.T) { 9 | _, customCfg := New() 10 | cut := customCfg.(*CustomConfigImpl) 11 | cut.VMetadataRepoUrl = "https://some-domain.com/bitbucket/scm/tmpl/service-metadata.git" 12 | repoName := cut.MetadataRepoName() 13 | require.Equal(t, "service-metadata", repoName) 14 | 15 | cut.VMetadataRepoUrl = "https://github.com/some-org/service-metadata-test.git" 16 | repoName = cut.MetadataRepoName() 17 | require.Equal(t, "service-metadata-test", repoName) 18 | } 19 | 20 | func TestMetadataRepoProject_MetadataRepoUrl(t *testing.T) { 21 | _, customCfg := New() 22 | cut := customCfg.(*CustomConfigImpl) 23 | cut.VMetadataRepoUrl = "https://some-domain.com/bitbucket/scm/tmpl/service-metadata.git" 24 | repoName := cut.MetadataRepoProject() 25 | require.Equal(t, "tmpl", repoName) 26 | 27 | cut.VMetadataRepoUrl = "https://github.com/some-org/service-metadata-test.git" 28 | repoName = cut.MetadataRepoProject() 29 | require.Equal(t, "some-org", repoName) 30 | } 31 | -------------------------------------------------------------------------------- /internal/repository/cache/servicecache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "github.com/Interhyp/metadata-service/api" 6 | ) 7 | 8 | const serviceWhat = "service" 9 | 10 | func (s *Impl) SetServiceListTimestamp(ctx context.Context, timestamp string) error { 11 | return s.setTimestamp(ctx, serviceWhat, serviceTimestampKey, timestamp) 12 | } 13 | 14 | func (s *Impl) GetServiceListTimestamp(ctx context.Context) (string, error) { 15 | return s.getTimestamp(ctx, serviceWhat, serviceTimestampKey) 16 | } 17 | 18 | func (s *Impl) GetSortedServiceNames(ctx context.Context) ([]string, error) { 19 | return getSortedKeys(ctx, serviceWhat, s, s.ServiceCache) 20 | } 21 | 22 | func (s *Impl) GetService(ctx context.Context, name string) (openapi.ServiceDto, error) { 23 | return getEntry(ctx, serviceWhat, s, s.ServiceCache, name) 24 | } 25 | 26 | func (s *Impl) PutService(ctx context.Context, name string, entry openapi.ServiceDto) error { 27 | return putEntry(ctx, serviceWhat, s, s.ServiceCache, name, entry) 28 | } 29 | 30 | func (s *Impl) DeleteService(ctx context.Context, name string) error { 31 | return removeEntry(ctx, serviceWhat, s, s.ServiceCache, name) 32 | } 33 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-create-cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a2430000000000", 3 | "configuration": { 4 | "accessKeys": [ 5 | { 6 | "key": "KEY", 7 | "permission": "REPO_WRITE" 8 | } 9 | ], 10 | "approvers": { 11 | "testing": [ 12 | "some-user" 13 | ] 14 | }, 15 | "commitMessageType": "SEMANTIC", 16 | "rawApprovers": { 17 | "testing": [ 18 | "some-user" 19 | ] 20 | }, 21 | "requireConditions": { 22 | "snyk-key": { 23 | "refMatcher": "master" 24 | } 25 | }, 26 | "requireIssue": false, 27 | "requireSuccessfulBuilds": 1, 28 | "webhooks": { 29 | "additional": [ 30 | { 31 | "events": [ 32 | "event" 33 | ], 34 | "name": "webhookname", 35 | "url": "webhookurl" 36 | } 37 | ] 38 | } 39 | }, 40 | "jiraIssue": "ISSUE-2345", 41 | "mainline": "master", 42 | "owner": "some-owner", 43 | "timeStamp": "2022-11-06T18:14:10Z", 44 | "type": "api", 45 | "url": "ssh://git@bitbucket.some-organisation.com:7999/helm/karma-wrapper.git" 46 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-update-cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a2430000000000", 3 | "configuration": { 4 | "accessKeys": [ 5 | { 6 | "key": "KEY", 7 | "permission": "REPO_WRITE" 8 | } 9 | ], 10 | "approvers": { 11 | "testing": [ 12 | "some-user" 13 | ] 14 | }, 15 | "commitMessageType": "SEMANTIC", 16 | "rawApprovers": { 17 | "testing": [ 18 | "some-user" 19 | ] 20 | }, 21 | "requireConditions": { 22 | "snyk-key": { 23 | "refMatcher": "master" 24 | } 25 | }, 26 | "requireIssue": false, 27 | "requireSuccessfulBuilds": 1, 28 | "webhooks": { 29 | "additional": [ 30 | { 31 | "events": [ 32 | "event" 33 | ], 34 | "name": "webhookname", 35 | "url": "webhookurl" 36 | } 37 | ] 38 | } 39 | }, 40 | "jiraIssue": "ISSUE-2345", 41 | "mainline": "master", 42 | "owner": "some-owner", 43 | "timeStamp": "2022-11-06T18:14:10Z", 44 | "type": "helm-chart", 45 | "url": "ssh://git@bitbucket.some-organisation.com:7999/helm/karma-wrapper.git" 46 | } -------------------------------------------------------------------------------- /api/generated_model_ref_protections_tags.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // RefProtectionsTags struct for RefProtectionsTags 15 | type RefProtectionsTags struct { 16 | // Prevents all changes of the protected refs. 17 | PreventAllChanges []ProtectedRef `yaml:"preventAllChanges,omitempty" json:"preventAllChanges,omitempty"` 18 | // Prevents creation of the protected refs. 19 | PreventCreation []ProtectedRef `yaml:"preventCreation,omitempty" json:"preventCreation,omitempty"` 20 | // Prevents deletion of the protected refs. 21 | PreventDeletion []ProtectedRef `yaml:"preventDeletion,omitempty" json:"preventDeletion,omitempty"` 22 | // Prevents force pushes to the protected refs for users with push permission. 23 | PreventForcePush []ProtectedRef `yaml:"preventForcePush,omitempty" json:"preventForcePush,omitempty"` 24 | } 25 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-update-newowner-cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a2430000000000", 3 | "configuration": { 4 | "accessKeys": [ 5 | { 6 | "key": "KEY", 7 | "permission": "REPO_WRITE" 8 | } 9 | ], 10 | "approvers": { 11 | "testing": [ 12 | "some-user" 13 | ] 14 | }, 15 | "commitMessageType": "SEMANTIC", 16 | "rawApprovers": { 17 | "testing": [ 18 | "some-user" 19 | ] 20 | }, 21 | "requireConditions": { 22 | "snyk-key": { 23 | "refMatcher": "master" 24 | } 25 | }, 26 | "requireIssue": false, 27 | "requireSuccessfulBuilds": 1, 28 | "webhooks": { 29 | "additional": [ 30 | { 31 | "events": [ 32 | "event" 33 | ], 34 | "name": "webhookname", 35 | "url": "webhookurl" 36 | } 37 | ] 38 | } 39 | }, 40 | "jiraIssue": "ISSUE-2345", 41 | "mainline": "master", 42 | "owner": "deleteme", 43 | "timeStamp": "2022-11-06T18:14:10Z", 44 | "type": "helm-chart", 45 | "url": "ssh://git@bitbucket.some-organisation.com:7999/helm/karma-wrapper.git" 46 | } -------------------------------------------------------------------------------- /api/generated_model_protected_ref.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // ProtectedRef struct for ProtectedRef 15 | type ProtectedRef struct { 16 | // fnmatch pattern to define protected refs. Must not start with refs/heads/ or refs/tags/. Special value :MAINLINE: matches the currently configured mainline for branch protections. 17 | Pattern string `yaml:"pattern" json:"pattern" validate:"regexp=^(?!refs\\/(heads|tags)\\/).*$"` 18 | // list of users or groups for which this protection does not apply. 19 | Exemptions []string `yaml:"exemptions,omitempty" json:"exemptions,omitempty"` 20 | // list of group identifiers for which this protection does not apply. This field is read-only and will be filled automatically from the exemptions fields. 21 | ExemptionsRoles []string `yaml:"exemptionsRoles,omitempty" json:"exemptionsRoles,omitempty"` 22 | } 23 | -------------------------------------------------------------------------------- /internal/repository/cache/repositorycache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "github.com/Interhyp/metadata-service/api" 6 | ) 7 | 8 | const repositoryWhat = "repository" 9 | 10 | func (s *Impl) SetRepositoryListTimestamp(ctx context.Context, timestamp string) error { 11 | return s.setTimestamp(ctx, repositoryWhat, repositoryTimestampKey, timestamp) 12 | } 13 | 14 | func (s *Impl) GetRepositoryListTimestamp(ctx context.Context) (string, error) { 15 | return s.getTimestamp(ctx, repositoryWhat, repositoryTimestampKey) 16 | } 17 | 18 | func (s *Impl) GetSortedRepositoryKeys(ctx context.Context) ([]string, error) { 19 | return getSortedKeys(ctx, repositoryWhat, s, s.RepositoryCache) 20 | } 21 | 22 | func (s *Impl) GetRepository(ctx context.Context, key string) (openapi.RepositoryDto, error) { 23 | return getEntry(ctx, repositoryWhat, s, s.RepositoryCache, key) 24 | } 25 | 26 | func (s *Impl) PutRepository(ctx context.Context, key string, entry openapi.RepositoryDto) error { 27 | return putEntry(ctx, repositoryWhat, s, s.RepositoryCache, key, entry) 28 | } 29 | 30 | func (s *Impl) DeleteRepository(ctx context.Context, key string) error { 31 | return removeEntry(ctx, repositoryWhat, s, s.RepositoryCache, key) 32 | } 33 | -------------------------------------------------------------------------------- /internal/service/check/yamlwalker_format.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "fmt" 5 | "github.com/go-git/go-billy/v5/util" 6 | "io/fs" 7 | ) 8 | 9 | func (v *MetadataWalker) FormatMetadata() error { 10 | return util.Walk(v.fs, v.config.rootDir, v.formatWalkFunc) 11 | } 12 | 13 | func (v *MetadataWalker) formatWalkFunc(path string, info fs.FileInfo, err error) error { 14 | return v.walkFunc( 15 | func(fileContents []byte) error { 16 | return v.formatSingleYamlFile(fileContents, path) 17 | })(path, info, err) 18 | } 19 | 20 | func (v *MetadataWalker) formatSingleYamlFile(fileContents []byte, path string) error { 21 | formatted, err := v.fmtEngine.FormatContent(fileContents) 22 | if err != nil { 23 | return err 24 | } 25 | if len(formatted) == 0 { 26 | return fmt.Errorf("missing formatter result") 27 | } 28 | return v.replaceFileContent(path, formatted) 29 | } 30 | 31 | func (v *MetadataWalker) replaceFileContent(path string, newContent []byte) error { 32 | err := v.fs.Remove(path) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | f, err := v.fs.Create(path) 38 | defer f.Close() 39 | if err != nil { 40 | return err 41 | } 42 | 43 | _, err = f.Write(newContent) 44 | if err != nil { 45 | return err 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/acorn/service/ownersint.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "github.com/Interhyp/metadata-service/api" 6 | ) 7 | 8 | // Owners provides the business logic for owner metadata. 9 | type Owners interface { 10 | IsOwners() bool 11 | 12 | Setup() error 13 | 14 | GetOwners(ctx context.Context) (openapi.OwnerListDto, error) 15 | GetOwner(ctx context.Context, ownerAlias string) (openapi.OwnerDto, error) 16 | 17 | GetAllGroupMembers(ctx context.Context, groupOwner string, groupName string) []string 18 | 19 | // CreateOwner returns the owner as it was created, with commit hash and timestamp filled in. 20 | CreateOwner(ctx context.Context, ownerAlias string, ownerDto openapi.OwnerCreateDto) (openapi.OwnerDto, error) 21 | 22 | // UpdateOwner returns the owner as it was committed, with commit hash and timestamp filled in. 23 | UpdateOwner(ctx context.Context, ownerAlias string, ownerDto openapi.OwnerDto) (openapi.OwnerDto, error) 24 | 25 | // PatchOwner returns the owner as it was committed, with commit hash and timestamp filled in. 26 | PatchOwner(ctx context.Context, ownerAlias string, ownerPatchDto openapi.OwnerPatchDto) (openapi.OwnerDto, error) 27 | 28 | DeleteOwner(ctx context.Context, ownerAlias string, deletionInfo openapi.DeletionDto) error 29 | } 30 | -------------------------------------------------------------------------------- /internal/acorn/errors/httperror/error.go: -------------------------------------------------------------------------------- 1 | package httperror 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | type Error interface { 9 | Ctx() context.Context 10 | IsHttpError() bool 11 | Status() int 12 | } 13 | 14 | // this also implements the error interface 15 | 16 | type Impl struct { 17 | ctx context.Context 18 | err error 19 | status int 20 | } 21 | 22 | func New(ctx context.Context, message string, status int) error { 23 | return &Impl{ 24 | ctx: ctx, 25 | err: fmt.Errorf(message), 26 | status: status, 27 | } 28 | } 29 | 30 | func Wrap(ctx context.Context, message string, status int, err error) error { 31 | wrappedError := fmt.Errorf("%s: %w", message, err) 32 | return &Impl{ 33 | ctx: ctx, 34 | err: wrappedError, 35 | status: status, 36 | } 37 | } 38 | 39 | func (e *Impl) Error() string { 40 | return e.err.Error() 41 | } 42 | 43 | func (e *Impl) Ctx() context.Context { 44 | return e.ctx 45 | } 46 | 47 | func (e *Impl) Status() int { 48 | return e.status 49 | } 50 | 51 | // the presence of this method makes the interface unique and thus recognizable by a simple type check 52 | 53 | func (e *Impl) IsHttpError() bool { 54 | return true 55 | } 56 | 57 | func Is(err error) bool { 58 | _, ok := err.(Error) 59 | return ok 60 | } 61 | -------------------------------------------------------------------------------- /api/generated_model_repository_create_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // RepositoryCreateDto struct for RepositoryCreateDto 15 | type RepositoryCreateDto struct { 16 | // The alias of the repository owner 17 | Owner string `yaml:"-" json:"owner"` 18 | Url string `yaml:"url" json:"url"` 19 | Mainline string `yaml:"mainline" json:"mainline"` 20 | // the generator used for the initial contents of this repository 21 | Generator *string `yaml:"generator,omitempty" json:"generator,omitempty"` 22 | Configuration *RepositoryConfigurationDto `yaml:"configuration,omitempty" json:"configuration,omitempty"` 23 | // The jira issue to use for committing a change, or the last jira issue used. 24 | JiraIssue string `yaml:"-" json:"jiraIssue"` 25 | // A map of arbitrary string labels attached to this repository. 26 | Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` 27 | } 28 | -------------------------------------------------------------------------------- /test/mock/kafkamock/kafka.go: -------------------------------------------------------------------------------- 1 | package kafkamock 2 | 3 | import ( 4 | "context" 5 | "github.com/Interhyp/metadata-service/internal/acorn/repository" 6 | ) 7 | import _ "github.com/go-git/go-git/v5" 8 | 9 | type Impl struct { 10 | Callback repository.ReceiverCallback 11 | 12 | Recording []repository.UpdateEvent 13 | } 14 | 15 | func New() repository.Kafka { 16 | return &Impl{ 17 | Callback: func(_ repository.UpdateEvent) {}, 18 | Recording: make([]repository.UpdateEvent, 0), 19 | } 20 | } 21 | 22 | func (r *Impl) IsKafka() bool { 23 | return true 24 | } 25 | 26 | func (r *Impl) Setup() error { 27 | return nil 28 | } 29 | 30 | func (r *Impl) Teardown() { 31 | } 32 | 33 | func (r *Impl) SubscribeIncoming(_ context.Context, callback repository.ReceiverCallback) error { 34 | r.Callback = callback 35 | return nil 36 | } 37 | 38 | func (r *Impl) Send(_ context.Context, event repository.UpdateEvent) error { 39 | r.Recording = append(r.Recording, event) 40 | return nil 41 | } 42 | 43 | func (r *Impl) StartReceiveLoop(ctx context.Context) error { 44 | return nil 45 | } 46 | 47 | // --- test helpers --- 48 | 49 | func (r *Impl) Receive(incomingEvent repository.UpdateEvent) { 50 | r.Callback(incomingEvent) 51 | } 52 | 53 | func (r *Impl) Reset() { 54 | r.Recording = make([]repository.UpdateEvent, 0) 55 | } 56 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/services.json: -------------------------------------------------------------------------------- 1 | { 2 | "services": { 3 | "some-service-backend": { 4 | "alertTarget": "https://webhook.com/9asdflk29d4m39g", 5 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 6 | "jiraIssue": "ISSUE-0000", 7 | "owner": "some-owner", 8 | "quicklinks": [ 9 | { 10 | "title": "Swagger UI", 11 | "url": "/swagger-ui/index.html" 12 | } 13 | ], 14 | "repositories": [ 15 | "some-service-backend.helm-deployment", 16 | "some-service-backend.implementation" 17 | ], 18 | "timeStamp": "2022-11-06T18:14:10Z" 19 | }, 20 | "some-service-backend-with-expandable-groups": { 21 | "alertTarget": "https://webhook.com/9asdflk29d4m39g", 22 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 23 | "jiraIssue": "ISSUE-0000", 24 | "owner": "some-owner", 25 | "quicklinks": [ 26 | { 27 | "title": "Swagger UI", 28 | "url": "/swagger-ui/index.html" 29 | } 30 | ], 31 | "repositories": [ 32 | "some-service-backend-with-expandable-groups.helm-deployment", 33 | "some-service-backend.implementation" 34 | ], 35 | "timeStamp": "2022-11-06T18:14:10Z" 36 | } 37 | }, 38 | "timeStamp": "2022-11-06T18:14:10Z" 39 | } -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 3 | "configuration": { 4 | "accessKeys": [ 5 | { 6 | "key": "DEPLOYMENT", 7 | "permission": "REPO_READ" 8 | }, 9 | { 10 | "data": "ssh-key abcdefgh.....", 11 | "permission": "REPO_WRITE" 12 | } 13 | ], 14 | "approvers": { 15 | "testing": [ 16 | "some-user" 17 | ] 18 | }, 19 | "commitMessageType": "DEFAULT", 20 | "mergeConfig": { 21 | "defaultStrategy": { 22 | "id": "no-ff" 23 | }, 24 | "strategies": [ 25 | { 26 | "id": "no-ff" 27 | }, 28 | { 29 | "id": "ff" 30 | }, 31 | { 32 | "id": "ff-only" 33 | }, 34 | { 35 | "id": "squash" 36 | } 37 | ] 38 | }, 39 | "rawApprovers": { 40 | "testing": [ 41 | "some-user" 42 | ] 43 | }, 44 | "requireIssue": true 45 | }, 46 | "generator": "third-party-software", 47 | "jiraIssue": "ISSUE-0000", 48 | "mainline": "main", 49 | "owner": "some-owner", 50 | "timeStamp": "2022-11-06T18:14:10Z", 51 | "type": "helm-deployment", 52 | "url": "ssh://git@bitbucket.some-organisation.com:7999/PROJECT/some-service-backend-deployment.git" 53 | } -------------------------------------------------------------------------------- /internal/acorn/repository/github.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "github.com/google/go-github/v70/github" 6 | ) 7 | 8 | type Github interface { 9 | StartCheckRun(ctx context.Context, owner, repoName, checkName, sha string) (int64, error) 10 | ConcludeCheckRun(ctx context.Context, owner, repoName, checkName string, checkRunId int64, conclusion CheckRunConclusion, details github.CheckRunOutput, actions ...*github.CheckRunAction) error 11 | GetUser(ctx context.Context, username string) (*github.User, error) 12 | CreateInstallationToken(ctx context.Context, installationId int64) (*github.InstallationToken, *github.Response, error) 13 | } 14 | 15 | type CheckRunConclusion string 16 | 17 | type CheckRunDetails struct { 18 | Title string 19 | Summary string 20 | BodyText string 21 | } 22 | 23 | const ( 24 | CheckRunSuccess CheckRunConclusion = "success" 25 | CheckRunFailure CheckRunConclusion = "failure" 26 | CheckRunActionRequired CheckRunConclusion = "action_required" 27 | CheckRunTimedOut CheckRunConclusion = "timed_out" 28 | CheckRunCancelled CheckRunConclusion = "cancelled" 29 | CheckRunNeutral CheckRunConclusion = "neutral" 30 | CheckRunSkipped CheckRunConclusion = "skipped" 31 | ) 32 | 33 | type CommitBuildStatus string 34 | 35 | type File struct { 36 | Path string 37 | Contents string 38 | } 39 | -------------------------------------------------------------------------------- /internal/service/util/UserGroupHelper.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func ParseGroupOwnerAndGroupName(mayBeGroupReference string) (bool, string, string) { 8 | hasGroupPrefix := strings.HasPrefix(mayBeGroupReference, "@") 9 | indexOfDot := strings.Index(mayBeGroupReference, ".") 10 | if hasGroupPrefix && indexOfDot > 0 { 11 | return true, mayBeGroupReference[1:indexOfDot], mayBeGroupReference[indexOfDot+1:] 12 | } 13 | return false, "", "" 14 | } 15 | 16 | func SplitUsersAndGroups(userAndGroups []string) ([]string, []string) { 17 | users := make([]string, 0) 18 | groups := make([]string, 0) 19 | for _, userOrGroup := range userAndGroups { 20 | isGroup, _, _ := ParseGroupOwnerAndGroupName(userOrGroup) 21 | if isGroup { 22 | groups = append(groups, userOrGroup) 23 | } else { 24 | users = append(users, userOrGroup) 25 | } 26 | } 27 | return users, groups 28 | } 29 | 30 | func Equal(a, b []string) bool { 31 | if len(a) != len(b) { 32 | return false 33 | } 34 | for i, v := range a { 35 | if v != b[i] { 36 | return false 37 | } 38 | } 39 | return true 40 | } 41 | 42 | func Difference(a, b []string) []string { 43 | mb := make(map[string]struct{}, len(b)) 44 | for _, x := range b { 45 | mb[x] = struct{}{} 46 | } 47 | var diff []string 48 | for _, x := range a { 49 | if _, found := mb[x]; !found { 50 | diff = append(diff, x) 51 | } 52 | } 53 | return diff 54 | } 55 | -------------------------------------------------------------------------------- /api/generated_model_ref_protections_branches.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // RefProtectionsBranches struct for RefProtectionsBranches 15 | type RefProtectionsBranches struct { 16 | // Forces creating a PR to update the protected refs. 17 | RequirePR []ProtectedRef `yaml:"requirePR,omitempty" json:"requirePR,omitempty"` 18 | // Prevents all changes of the protected refs. 19 | PreventAllChanges []ProtectedRef `yaml:"preventAllChanges,omitempty" json:"preventAllChanges,omitempty"` 20 | // Prevents creation of the protected refs. 21 | PreventCreation []ProtectedRef `yaml:"preventCreation,omitempty" json:"preventCreation,omitempty"` 22 | // Prevents deletion of the protected refs. 23 | PreventDeletion []ProtectedRef `yaml:"preventDeletion,omitempty" json:"preventDeletion,omitempty"` 24 | // Prevents pushes to the protected refs. 25 | PreventPush []ProtectedRef `yaml:"preventPush,omitempty" json:"preventPush,omitempty"` 26 | // Prevents force pushes to the protected refs for users with push permission. 27 | PreventForcePush []ProtectedRef `yaml:"preventForcePush,omitempty" json:"preventForcePush,omitempty"` 28 | } 29 | -------------------------------------------------------------------------------- /test/resources/invalid-config-values.yaml: -------------------------------------------------------------------------------- 1 | APPLICATION_NAME: 'rm -rf something' 2 | PLATFORM: spaceship 3 | ENVIRONMENT: mars 4 | 5 | SERVER_ADDRESS: 'Total Nonsense' 6 | SERVER_PORT: '122834' 7 | METRICS_PORT: '-12387192873invalid' 8 | 9 | # have to set this valid or test can't run 10 | LOGSTYLE: plain 11 | 12 | VAULT_ENABLED: what 13 | VAULT_SERVER: https://something 14 | VAULT_AUTH_TOKEN: not a token 15 | VAULT_SECRETS_CONFIG: '{}}' 16 | 17 | UPDATE_JOB_INTERVAL_MINUTES: 26 18 | UPDATE_JOB_TIMEOUT_SECONDS: true 19 | 20 | KAFKA_GROUP_ID_OVERRIDE: 'no banana, no spaces' 21 | 22 | NOTIFICATION_CONSUMER_CONFIGS: >- 23 | { 24 | "caseMissingUrl": { 25 | "types": { 26 | "Owner": ["CREATED", "MODIFIED", "DELETED"] 27 | } 28 | }, 29 | "caseInvalidUrl": { 30 | "types": { 31 | "Owner": ["CREATED", "MODIFIED", "DELETED"] 32 | }, 33 | "url": "this-is-invalid" 34 | }, 35 | "caseInvalidTypes": { 36 | "types": { 37 | "invalid": ["CREATED", "MODIFIED", "DELETED"], 38 | "alsoInvalid": ["CREATED"] 39 | }, 40 | "url": "https://some.url.com/for/the/webhook" 41 | }, 42 | "caseInvalidEvents": { 43 | "types": { 44 | "Owner": ["INVALID", "ALSO_INVALID"], 45 | "Service": ["CREATED", "AGAIN_INVALID"] 46 | }, 47 | "url": "https://another.url.com/for/another/webhook" 48 | }, 49 | "allValid": { 50 | "types": { 51 | "Owner": ["CREATED", "DELETED"] 52 | }, 53 | "url": "https://valid.url.com/for/a/webhook" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/web/util/paramhelper.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/Interhyp/go-backend-service-common/acorns/repository" 8 | "github.com/Interhyp/go-backend-service-common/api/apierrors" 9 | "github.com/Interhyp/metadata-service/api" 10 | aulogging "github.com/StephanHCB/go-autumn-logging" 11 | "github.com/go-chi/chi/v5" 12 | "net/http" 13 | "net/url" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | func StringPathParam(r *http.Request, key string) string { 19 | return chi.URLParam(r, key) 20 | } 21 | 22 | func StringQueryParam(r *http.Request, key string) string { 23 | query := r.URL.Query() 24 | return query.Get(key) 25 | } 26 | 27 | func NonEmptyStringPathParam(_ context.Context, r *http.Request, key string, timestamp repository.Timestamp) (string, error) { 28 | param, _ := url.QueryUnescape(chi.URLParam(r, key)) 29 | if strings.TrimSpace(param) == "" { 30 | return "", apierrors.NewBadRequestError("invalid.path.param", fmt.Sprintf("path param %s must be non empty", key), nil, timestamp.Now()) 31 | } 32 | return param, nil 33 | } 34 | 35 | func ParseBodyToDeletionDto(ctx context.Context, r *http.Request, timestamp time.Time) (openapi.DeletionDto, error) { 36 | decoder := json.NewDecoder(r.Body) 37 | dto := openapi.DeletionDto{} 38 | err := decoder.Decode(&dto) 39 | if err != nil { 40 | aulogging.Logger.Ctx(ctx).Info().Printf("deletion body invalid: %s", err.Error()) 41 | return openapi.DeletionDto{}, apierrors.NewBadRequestError("deletion.invalid.body", "body failed to parse", err, timestamp) 42 | } 43 | return dto, nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/web/server/metricsserver.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/prometheus/client_golang/prometheus/promhttp" 10 | ) 11 | 12 | // we use this with a go-routine, as it never returns 13 | func (s *Impl) metricsServerListenAndServe(ctx context.Context, srv *http.Server) { 14 | s.Logging.Logger().Ctx(ctx).Info().Print("starting metrics http server") 15 | err := srv.ListenAndServe() 16 | if err != nil && !errors.Is(err, http.ErrServerClosed) { 17 | s.Logging.Logger().NoCtx().Error().WithErr(err).Print("failed to start background metrics server. BAILING OUT") 18 | s.Logging.Logger().NoCtx().Fatal().WithErr(err).Print("error was: " + err.Error()) 19 | } 20 | s.Logging.Logger().NoCtx().Info().Print("metrics http server has shut down") 21 | } 22 | 23 | func (s *Impl) StartMetricsServerAsyncTerminatesOnError(ctx context.Context, srv *http.Server) { 24 | if srv != nil { 25 | go s.metricsServerListenAndServe(ctx, srv) 26 | } 27 | } 28 | 29 | func (s *Impl) CreateMetricsServer(ctx context.Context) *http.Server { 30 | if s.Configuration.MetricsPort() > 0 { 31 | address := fmt.Sprintf("%s:%d", s.Configuration.ServerAddress(), s.Configuration.MetricsPort()) 32 | s.Logging.Logger().Ctx(ctx).Info().Print("creating metrics http server on " + address) 33 | metricsServeMux := http.NewServeMux() 34 | metricsServeMux.Handle("/metrics", promhttp.Handler()) 35 | return s.NewServer(ctx, address, metricsServeMux) 36 | } else { 37 | s.Logging.Logger().Ctx(ctx).Info().Print("will not start metrics http server - no metrics port configured") 38 | return nil 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/web/controller/webhookctl/webhookctl.go: -------------------------------------------------------------------------------- 1 | package webhookctl 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/Interhyp/go-backend-service-common/api/apierrors" 7 | "github.com/Interhyp/metadata-service/internal/acorn/controller" 8 | "github.com/Interhyp/metadata-service/internal/acorn/service" 9 | "net/http" 10 | 11 | librepo "github.com/Interhyp/go-backend-service-common/acorns/repository" 12 | "github.com/Interhyp/metadata-service/internal/web/util" 13 | "github.com/go-chi/chi/v5" 14 | ) 15 | 16 | type Impl struct { 17 | Logging librepo.Logging 18 | Timestamp librepo.Timestamp 19 | WebhooksHandler service.WebhooksHandler 20 | } 21 | 22 | func New( 23 | logging librepo.Logging, 24 | timestamp librepo.Timestamp, 25 | webhookshandler service.WebhooksHandler, 26 | ) controller.WebhookController { 27 | return &Impl{ 28 | Logging: logging, 29 | Timestamp: timestamp, 30 | WebhooksHandler: webhookshandler, 31 | } 32 | } 33 | 34 | func (c *Impl) IsWebhookController() bool { 35 | return true 36 | } 37 | 38 | func (c *Impl) WireUp(_ context.Context, router chi.Router) { 39 | router.Post(fmt.Sprintf("/webhooks/vcs/github"), c.PostGithubWebhook) 40 | } 41 | 42 | // --- handlers --- 43 | 44 | func (c *Impl) PostGithubWebhook(w http.ResponseWriter, r *http.Request) { 45 | ctx := r.Context() 46 | 47 | if err := c.WebhooksHandler.HandleEvent(ctx, r); err != nil { 48 | apierrors.HandleError(ctx, w, r, err, 49 | apierrors.IsBadRequestError, 50 | apierrors.IsBadGatewayError, 51 | apierrors.IsInternalServerError) 52 | return 53 | } 54 | util.SuccessNoBody(ctx, w, r, http.StatusNoContent) 55 | } 56 | -------------------------------------------------------------------------------- /internal/acorn/service/servicesint.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "github.com/Interhyp/metadata-service/api" 6 | ) 7 | 8 | // Services provides the business logic for service metadata. 9 | type Services interface { 10 | IsServices() bool 11 | 12 | Setup() error 13 | 14 | GetServices(ctx context.Context, ownerAliasFilter string) (openapi.ServiceListDto, error) 15 | GetService(ctx context.Context, serviceName string) (openapi.ServiceDto, error) 16 | 17 | // CreateService returns the service as it was created, with commit hash and timestamp filled in. 18 | CreateService(ctx context.Context, serviceName string, serviceDto openapi.ServiceCreateDto) (openapi.ServiceDto, error) 19 | 20 | // UpdateService returns the service as it was committed, with commit hash and timestamp filled in. 21 | // 22 | // Changing the owner of a service is supported, and will also move any referenced repositories to the new owner. 23 | UpdateService(ctx context.Context, serviceName string, serviceDto openapi.ServiceDto) (openapi.ServiceDto, error) 24 | 25 | // PatchService returns the service as it was committed, with commit hash and timestamp filled in. 26 | // 27 | // Changing the owner of a service is supported, and will also move any referenced repositories to the new owner. 28 | PatchService(ctx context.Context, serviceName string, servicePatchDto openapi.ServicePatchDto) (openapi.ServiceDto, error) 29 | 30 | // DeleteService deletes a service, but leaves its repositories behind 31 | // 32 | // Reason: they still need to be configured by bit-brother. 33 | DeleteService(ctx context.Context, serviceName string, deletionInfo openapi.DeletionDto) error 34 | } 35 | -------------------------------------------------------------------------------- /internal/acorn/repository/kafkaint.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import "context" 4 | 5 | // Kafka is the central singleton representing the kafka messaging bus. 6 | type Kafka interface { 7 | IsKafka() bool 8 | 9 | // Setup only connects the producer, the consumer is connected with StartReceiveLoop. 10 | Setup() error 11 | // Teardown will close both producer and consumer if they have been connected. 12 | Teardown() 13 | 14 | // SubscribeIncoming allows you to register a callback that is called whenever a message is received from the Kafka bus. 15 | // 16 | // Note, we currently only allow a single callback, so calling this multiple times will overwrite the callback. 17 | // Use this during application setup. 18 | SubscribeIncoming(ctx context.Context, callback ReceiverCallback) error 19 | 20 | // Send sends an UpdateEvent that originates in this application to the Kafka bus. 21 | Send(ctx context.Context, event UpdateEvent) error 22 | 23 | // StartReceiveLoop starts a background goroutine that calls the subscribed callback when messages come in 24 | StartReceiveLoop(ctx context.Context) error 25 | } 26 | 27 | type ReceiverCallback func(event UpdateEvent) 28 | 29 | type UpdateEvent struct { 30 | Affected EventAffects `json:"affected"` 31 | 32 | // ISO-8601 UTC date time at which this information was committed. 33 | TimeStamp string `json:"timeStamp"` 34 | // The git commit hash this information was committed under. 35 | CommitHash string `json:"commitHash"` 36 | } 37 | 38 | type EventAffects struct { 39 | OwnerAliases []string `json:"ownerAliases"` 40 | ServiceNames []string `json:"serviceNames"` 41 | RepositoryKeys []string `json:"repositoryKeys"` 42 | } 43 | -------------------------------------------------------------------------------- /internal/repository/hostip/hostip.go: -------------------------------------------------------------------------------- 1 | package hostip 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | librepo "github.com/Interhyp/go-backend-service-common/acorns/repository" 7 | "github.com/Interhyp/metadata-service/internal/acorn/repository" 8 | auzerolog "github.com/StephanHCB/go-autumn-logging-zerolog" 9 | "net" 10 | ) 11 | 12 | type Impl struct { 13 | Logging librepo.Logging 14 | } 15 | 16 | func New( 17 | logging librepo.Logging, 18 | ) repository.HostIP { 19 | return &Impl{ 20 | Logging: logging, 21 | } 22 | } 23 | 24 | func (r *Impl) IsHostIP() bool { 25 | return true 26 | } 27 | 28 | func (r *Impl) Setup() error { 29 | ctx := auzerolog.AddLoggerToCtx(context.Background()) 30 | 31 | ip, err := r.ObtainLocalIp() 32 | if err != nil { 33 | r.Logging.Logger().Ctx(ctx).Error().WithErr(err).Print("failed to obtain local ip address. BAILING OUT") 34 | return err 35 | } 36 | 37 | r.Logging.Logger().Ctx(ctx).Info().Printf("non-trivial ipv4 address is %s", ip.String()) 38 | 39 | return nil 40 | } 41 | 42 | func (r *Impl) ObtainLocalIp() (net.IP, error) { 43 | ifaces, err := net.Interfaces() 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | for _, i := range ifaces { 49 | addrs, err := i.Addrs() 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | for _, addr := range addrs { 55 | var ip net.IP 56 | switch v := addr.(type) { 57 | case *net.IPNet: 58 | ip = v.IP 59 | case *net.IPAddr: 60 | ip = v.IP 61 | } 62 | 63 | if ip.To4() != nil { 64 | if !ip.IsLoopback() { 65 | return ip.To4(), nil 66 | } 67 | } 68 | } 69 | } 70 | 71 | return nil, errors.New("could not determine local IPv4 address, not localhost") 72 | } 73 | -------------------------------------------------------------------------------- /api/generated_model_repository_patch_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // RepositoryPatchDto struct for RepositoryPatchDto 15 | type RepositoryPatchDto struct { 16 | // The alias of the repository owner 17 | Owner *string `yaml:"owner,omitempty" json:"owner,omitempty"` 18 | Url *string `yaml:"url,omitempty" json:"url,omitempty"` 19 | Mainline *string `yaml:"mainline,omitempty" json:"mainline,omitempty"` 20 | // the generator used for the initial contents of this repository 21 | Generator *string `yaml:"generator,omitempty" json:"generator,omitempty"` 22 | Configuration *RepositoryConfigurationPatchDto `yaml:"configuration,omitempty" json:"configuration,omitempty"` 23 | // ISO-8601 UTC date time at which this information was originally committed. When sending an update, include the original timestamp you got so we can detect concurrent updates. 24 | TimeStamp string `yaml:"-" json:"timeStamp"` 25 | // The git commit hash this information was originally committed under. When sending an update, include the original commitHash you got so we can detect concurrent updates. 26 | CommitHash string `yaml:"-" json:"commitHash"` 27 | // The jira issue to use for committing a change, or the last jira issue used. 28 | JiraIssue string `yaml:"-" json:"jiraIssue"` 29 | // A map of arbitrary string labels attached to this repository. 30 | Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` 31 | } 32 | -------------------------------------------------------------------------------- /test/acceptance/startup_acc_test.go: -------------------------------------------------------------------------------- 1 | package acceptance 2 | 3 | import ( 4 | "github.com/Interhyp/go-backend-service-common/docs" 5 | "github.com/stretchr/testify/require" 6 | "net/http" 7 | "testing" 8 | ) 9 | 10 | func TestStartup_ShouldBeHealthy(t *testing.T) { 11 | docs.Given("Given a valid configuration") 12 | 13 | docs.When("When the application is started") 14 | 15 | docs.Then("Then all components are present and of the correct type") 16 | require.NotNil(t, application.Config) 17 | require.NotNil(t, application.CustomConfig) 18 | require.NotNil(t, application.Logging) 19 | require.NotNil(t, application.Vault) 20 | require.NotNil(t, application.Kafka) 21 | require.NotNil(t, application.Metadata) 22 | require.NotNil(t, application.HostIP) 23 | 24 | require.NotNil(t, application.Cache) 25 | require.NotNil(t, application.Mapper) 26 | require.NotNil(t, application.Trigger) 27 | require.NotNil(t, application.Updater) 28 | require.NotNil(t, application.Owners) 29 | require.NotNil(t, application.Services) 30 | require.NotNil(t, application.Repositories) 31 | 32 | require.NotNil(t, application.HealthCtl) 33 | require.NotNil(t, application.SwaggerCtl) 34 | require.NotNil(t, application.OwnerCtl) 35 | require.NotNil(t, application.ServiceCtl) 36 | require.NotNil(t, application.RepositoryCtl) 37 | require.NotNil(t, application.WebhookCtl) 38 | 39 | require.NotNil(t, application.Server) 40 | 41 | docs.Then("And the application reports as healthy") 42 | response, err := tstPerformGet("/management/health", tstUnauthenticated()) 43 | tstAssert(t, response, err, http.StatusOK, "health.json") 44 | 45 | response, err = tstPerformGet("/health", tstUnauthenticated()) 46 | tstAssert(t, response, err, http.StatusOK, "health.json") 47 | 48 | response, err = tstPerformGet("/", tstUnauthenticated()) 49 | tstAssert(t, response, err, http.StatusOK, "health.json") 50 | } 51 | -------------------------------------------------------------------------------- /api/generated_model_repository_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // RepositoryDto struct for RepositoryDto 15 | type RepositoryDto struct { 16 | // The type of the repository as determined by its key. 17 | Type *string `yaml:"-" json:"type,omitempty"` 18 | // The alias of the repository owner 19 | Owner string `yaml:"-" json:"owner"` 20 | Description *string `yaml:"description,omitempty" json:"description,omitempty"` 21 | Url string `yaml:"url" json:"url"` 22 | Mainline string `yaml:"mainline" json:"mainline"` 23 | // the generator used for the initial contents of this repository 24 | Generator *string `yaml:"generator,omitempty" json:"generator,omitempty"` 25 | Configuration *RepositoryConfigurationDto `yaml:"configuration,omitempty" json:"configuration,omitempty"` 26 | // ISO-8601 UTC date time at which this information was originally committed. When sending an update, include the original timestamp you got so we can detect concurrent updates. 27 | TimeStamp string `yaml:"-" json:"timeStamp"` 28 | // The git commit hash this information was originally committed under. When sending an update, include the original commitHash you got so we can detect concurrent updates. 29 | CommitHash string `yaml:"-" json:"commitHash"` 30 | // The jira issue to use for committing a change, or the last jira issue used. 31 | JiraIssue string `yaml:"-" json:"jiraIssue"` 32 | // A map of arbitrary string labels attached to this repository. 33 | Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` 34 | } 35 | -------------------------------------------------------------------------------- /api/generated_model_owner_create_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // OwnerCreateDto struct for OwnerCreateDto 15 | type OwnerCreateDto struct { 16 | // The contact information of the owner 17 | Contact string `yaml:"contact" json:"contact"` 18 | // The teams channel url information of the owner 19 | TeamsChannelURL *string `yaml:"teamsChannelURL,omitempty" json:"teamsChannelURL,omitempty"` 20 | // The product owner of this owner space 21 | ProductOwner *string `yaml:"productOwner,omitempty" json:"productOwner,omitempty"` 22 | // A list of users that are allowed to promote services in this owner space 23 | Promoters []string `yaml:"promoters,omitempty" json:"promoters,omitempty"` 24 | // A list of users which constitute this owner 25 | Members []string `yaml:"members,omitempty" json:"members,omitempty"` 26 | // Map of string (group name e.g. some-owner) of strings (list of usernames), one username for each group is required. 27 | Groups map[string][]string `yaml:"groups,omitempty" json:"groups,omitempty"` 28 | // The default jira project that is used by this owner space 29 | DefaultJiraProject *string `yaml:"defaultJiraProject,omitempty" json:"defaultJiraProject,omitempty"` 30 | // The jira issue to use for committing a change, or the last jira issue used. 31 | JiraIssue string `yaml:"-" json:"jiraIssue"` 32 | // A display name of the owner, to be presented in user interfaces instead of the owner's name, when available 33 | DisplayName *string `yaml:"displayName,omitempty" json:"displayName,omitempty"` 34 | Links []Link `yaml:"links,omitempty" json:"links,omitempty"` 35 | } 36 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repository-expanded-groups.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 3 | "configuration": { 4 | "accessKeys": [ 5 | { 6 | "key": "DEPLOYMENT", 7 | "permission": "REPO_READ" 8 | }, 9 | { 10 | "data": "ssh-key abcdefgh.....", 11 | "permission": "REPO_WRITE" 12 | } 13 | ], 14 | "approvers": { 15 | "testing": [ 16 | "some-other-user", 17 | "a-very-special-user" 18 | ] 19 | }, 20 | "commitMessageType": "DEFAULT", 21 | "mergeConfig": { 22 | "defaultStrategy": { 23 | "id": "no-ff" 24 | }, 25 | "strategies": [ 26 | { 27 | "id": "no-ff" 28 | }, 29 | { 30 | "id": "ff" 31 | }, 32 | { 33 | "id": "ff-only" 34 | }, 35 | { 36 | "id": "squash" 37 | } 38 | ] 39 | }, 40 | "rawApprovers": { 41 | "testing": [ 42 | "@some-owner.users" 43 | ] 44 | }, 45 | "rawWatchers": [ 46 | "@some-owner.users" 47 | ], 48 | "refProtections": { 49 | "branches": { 50 | "requirePR": [ 51 | { 52 | "exemptions": [ 53 | "some-other-user", 54 | "a-very-special-user" 55 | ], 56 | "exemptionsRoles": [ 57 | "@some-owner.users" 58 | ], 59 | "pattern": ":MAINLINE:" 60 | } 61 | ] 62 | } 63 | }, 64 | "requireIssue": true, 65 | "watchers": [ 66 | "some-other-user", 67 | "a-very-special-user" 68 | ] 69 | }, 70 | "generator": "third-party-software", 71 | "jiraIssue": "ISSUE-0000", 72 | "mainline": "main", 73 | "owner": "some-owner", 74 | "timeStamp": "2022-11-06T18:14:10Z", 75 | "type": "helm-deployment", 76 | "url": "ssh://git@bitbucket.some-organisation.com:7999/PROJECT/some-service-backend-with-expandable-groups-deployment.git" 77 | } -------------------------------------------------------------------------------- /test/mock/idpmock/idp.go: -------------------------------------------------------------------------------- 1 | package idpmock 2 | 3 | import ( 4 | "context" 5 | "github.com/Interhyp/metadata-service/internal/acorn/repository" 6 | ) 7 | import _ "github.com/go-git/go-git/v5" 8 | 9 | type Impl struct{} 10 | 11 | func New() repository.IdentityProvider { 12 | return &Impl{} 13 | } 14 | 15 | func (r *Impl) IsIdentityProvider() bool { 16 | return true 17 | } 18 | 19 | // We created a keyset and some tokens using jwt.io. Use this keyset ONLY for automated tests! 20 | 21 | // public key that can verify our testing tokens 22 | const pemKey1 = `-----BEGIN PUBLIC KEY----- 23 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo 24 | 4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u 25 | +qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh 26 | kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ 27 | 0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg 28 | cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc 29 | mwIDAQAB 30 | -----END PUBLIC KEY----- 31 | ` 32 | 33 | // just a key we use to test having multiple keys in the key set 34 | const pemKey2 = `-----BEGIN PUBLIC KEY----- 35 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo 36 | 4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u 37 | +qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh 38 | kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ 39 | 0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg 40 | cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc 41 | mwIDAQXX 42 | -----END PUBLIC KEY----- 43 | ` 44 | 45 | func (r *Impl) Setup() error { 46 | return nil 47 | } 48 | 49 | func (r *Impl) SetupConnector(ctx context.Context) error { 50 | return nil 51 | } 52 | 53 | func (r *Impl) ObtainKeySet(ctx context.Context) error { 54 | return nil 55 | } 56 | 57 | func (r *Impl) GetKeySet(ctx context.Context) []string { 58 | return []string{pemKey2, pemKey1} 59 | } 60 | 61 | func (r *Impl) VerifyToken(ctx context.Context, token string) error { 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /test/resources/acceptance-expected/repositories-filtered-service.json: -------------------------------------------------------------------------------- 1 | { 2 | "repositories": { 3 | "some-service-backend.helm-deployment": { 4 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 5 | "configuration": { 6 | "accessKeys": [ 7 | { 8 | "key": "DEPLOYMENT", 9 | "permission": "REPO_READ" 10 | }, 11 | { 12 | "data": "ssh-key abcdefgh.....", 13 | "permission": "REPO_WRITE" 14 | } 15 | ], 16 | "approvers": { 17 | "testing": [ 18 | "some-user" 19 | ] 20 | }, 21 | "commitMessageType": "DEFAULT", 22 | "mergeConfig": { 23 | "defaultStrategy": { 24 | "id": "no-ff" 25 | }, 26 | "strategies": [ 27 | { 28 | "id": "no-ff" 29 | }, 30 | { 31 | "id": "ff" 32 | }, 33 | { 34 | "id": "ff-only" 35 | }, 36 | { 37 | "id": "squash" 38 | } 39 | ] 40 | }, 41 | "rawApprovers": { 42 | "testing": [ 43 | "some-user" 44 | ] 45 | }, 46 | "requireIssue": true 47 | }, 48 | "generator": "third-party-software", 49 | "jiraIssue": "ISSUE-0000", 50 | "mainline": "main", 51 | "owner": "some-owner", 52 | "timeStamp": "2022-11-06T18:14:10Z", 53 | "type": "helm-deployment", 54 | "url": "ssh://git@bitbucket.some-organisation.com:7999/PROJECT/some-service-backend-deployment.git" 55 | }, 56 | "some-service-backend.implementation": { 57 | "commitHash": "6c8ac2c35791edf9979623c717a243fc53400000", 58 | "generator": "java-spring-cloud", 59 | "jiraIssue": "ISSUE-0000", 60 | "mainline": "master", 61 | "owner": "some-owner", 62 | "timeStamp": "2022-11-06T18:14:10Z", 63 | "type": "implementation", 64 | "url": "ssh://git@bitbucket.some-organisation.com:7999/PROJECT/some-service-backend.git" 65 | } 66 | }, 67 | "timeStamp": "2022-11-06T18:14:10Z" 68 | } -------------------------------------------------------------------------------- /internal/acorn/service/repositoriesint.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "github.com/Interhyp/go-backend-service-common/api/apierrors" 6 | "github.com/Interhyp/metadata-service/api" 7 | ) 8 | 9 | // Repositories provides the business logic for repository metadata. 10 | type Repositories interface { 11 | IsRepositories() bool 12 | Setup() error 13 | 14 | // ValidRepositoryKey checks validity of a repository key and returns an error describing the problem if invalid 15 | ValidRepositoryKey(ctx context.Context, repoKey string) apierrors.AnnotatedError 16 | 17 | GetRepositories(ctx context.Context, 18 | ownerAliasFilter string, serviceNameFilter string, 19 | nameFilter string, typeFilter string, 20 | urlFilter string) (openapi.RepositoryListDto, error) 21 | GetRepository(ctx context.Context, repoKey string) (openapi.RepositoryDto, error) 22 | 23 | // CreateRepository returns the repository as it was created, with commit hash and timestamp filled in. 24 | CreateRepository(ctx context.Context, key string, repositoryDto openapi.RepositoryCreateDto) (openapi.RepositoryDto, error) 25 | 26 | // UpdateRepository returns the repository as it was committed, with commit hash and timestamp filled in. 27 | // 28 | // Changing the owner of a repository is supported, unless it's still referenced by its service. In that case, 29 | // move the whole service (including its repositories). 30 | UpdateRepository(ctx context.Context, key string, repositoryDto openapi.RepositoryDto) (openapi.RepositoryDto, error) 31 | 32 | // PatchRepository returns the repository as it was committed, with commit hash and timestamp filled in. 33 | // 34 | // Changing the owner of a repository is supported, unless it's still referenced by its service. In that case, 35 | // move the whole service (including its repositories). 36 | PatchRepository(ctx context.Context, key string, repositoryPatchDto openapi.RepositoryPatchDto) (openapi.RepositoryDto, error) 37 | 38 | // DeleteRepository will fail if the repo is still referenced by its service. Delete that one first. 39 | DeleteRepository(ctx context.Context, key string, deletionInfo openapi.DeletionDto) error 40 | } 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 18 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 19 | .idea/ 20 | *.iml 21 | 22 | # Gradle and Maven with auto-import 23 | # When using Gradle or Maven with auto-import, you should exclude module files, 24 | # since they will be recreated, and may cause churn. Uncomment if using 25 | # auto-import. 26 | # .idea/artifacts 27 | # .idea/compiler.xml 28 | # .idea/jarRepositories.xml 29 | # .idea/modules.xml 30 | # .idea/*.iml 31 | # .idea/modules 32 | # *.iml 33 | # *.ipr 34 | 35 | # CMake 36 | cmake-build-*/ 37 | 38 | # Mongo Explorer plugin 39 | .idea/**/mongoSettings.xml 40 | 41 | # File-based project format 42 | *.iws 43 | 44 | # IntelliJ 45 | out/ 46 | 47 | # mpeltonen/sbt-idea plugin 48 | .idea_modules/ 49 | 50 | # JIRA plugin 51 | atlassian-ide-plugin.xml 52 | 53 | # Cursive Clojure plugin 54 | .idea/replstate.xml 55 | 56 | # SonarLint plugin 57 | .idea/sonarlint/ 58 | 59 | # Crashlytics plugin (for Android Studio and IntelliJ) 60 | com_crashlytics_export_strings.xml 61 | crashlytics.properties 62 | crashlytics-build.properties 63 | fabric.properties 64 | 65 | # Editor-based Rest Client 66 | .idea/httpRequests 67 | 68 | # Android studio 3.1+ serialized cache file 69 | .idea/caches/build_file_checksums.ser 70 | 71 | # Compiled class file 72 | *.class 73 | 74 | # Log file 75 | *.log 76 | 77 | # BlueJ files 78 | *.ctxt 79 | 80 | # Mobile Tools for Java (J2ME) 81 | .mtj.tmp/ 82 | 83 | # Package Files # 84 | *.jar 85 | *.war 86 | *.nar 87 | *.ear 88 | *.zip 89 | *.tar.gz 90 | *.rar 91 | 92 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 93 | hs_err_pid* 94 | replay_pid* 95 | 96 | # local config file 97 | local-config.yaml 98 | 99 | .gitignored/* 100 | .gitignored/**/* -------------------------------------------------------------------------------- /api-generator/generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ -d "api-generator" ]; then 6 | cd api-generator 7 | fi 8 | 9 | GENERATOR_VERSION=7.7.1_INTERHYP 10 | GENERATOR_NAME=openapi-generator-cli 11 | GENERATOR=$GENERATOR_NAME-$GENERATOR_VERSION.jar 12 | 13 | if [ ! -f "$GENERATOR" ]; then 14 | curl -k https://raw.githubusercontent.com/Interhyp/openapi-generator/refs/heads/new_generator_rebased/bin/$GENERATOR > $GENERATOR 15 | fi 16 | 17 | API_MODEL_PACKAGE_NAME=openapi 18 | 19 | function generate_apimodel { 20 | java -jar $GENERATOR generate \ 21 | -i ../api/openapi-v3-spec.yaml \ 22 | -o ../api \ 23 | --package-name $API_MODEL_PACKAGE_NAME \ 24 | --global-property models \ 25 | --additional-properties=enumClassPrefix=true,structPrefix=true \ 26 | -g go-autumrest 27 | } 28 | 29 | DOWNSTREAM_API_DIRECTORY=../internal/client 30 | 31 | function generate_downstream { 32 | P_DOWNSTREAM_NAME=$1 33 | P_SPEC_FILE_NAME=$2 34 | # use 'tags' from openapi to generate only selected parts of the entire api. Use ':' as separator for multiple values. Convert whitespaces to CamelCased string: 'Abc and Def'->'AbcAndDef' 35 | P_APIS=$3 36 | 37 | MODEL_PACKAGE_NAME=${P_DOWNSTREAM_NAME}client 38 | java -jar $GENERATOR generate \ 39 | -i ${DOWNSTREAM_API_DIRECTORY}/${P_SPEC_FILE_NAME} \ 40 | -o ${DOWNSTREAM_API_DIRECTORY}/${P_DOWNSTREAM_NAME} \ 41 | --package-name ${MODEL_PACKAGE_NAME} \ 42 | --global-property supportingFiles,models,apis=${P_APIS} \ 43 | --additional-properties=enumClassPrefix=true,structPrefix=true \ 44 | -g go-autumrest 45 | } 46 | 47 | generate_apimodel 48 | 49 | # -------------------------------------- customization ----------------------------------------- 50 | # omit certain fields from yaml representations, which we use internally to save to files in git 51 | # (this information is represented in the directory tree or is part of the commit metadata) 52 | for i in ../api/*.go; do 53 | sed -i'' -e 's/yaml:"timeStamp"/yaml:"-"/g' $i 54 | sed -i'' -e 's/yaml:"commitHash"/yaml:"-"/g' $i 55 | sed -i'' -e 's/yaml:"jiraIssue"/yaml:"-"/g' $i 56 | sed -i'' -e 's/yaml:"owner"/yaml:"-"/g' $i 57 | sed -i'' -e 's/yaml:"type,omitempty"/yaml:"-"/g' $i 58 | done 59 | 60 | # ------------------------------------ end customization --------------------------------------- 61 | 62 | gofmt -w ../api/*.go 63 | -------------------------------------------------------------------------------- /api/generated_model_owner_patch_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // OwnerPatchDto struct for OwnerPatchDto 15 | type OwnerPatchDto struct { 16 | // The contact information of the owner 17 | Contact *string `yaml:"contact,omitempty" json:"contact,omitempty"` 18 | // The teams channel url information of the owner 19 | TeamsChannelURL *string `yaml:"teamsChannelURL,omitempty" json:"teamsChannelURL,omitempty"` 20 | // The product owner of this owner space 21 | ProductOwner *string `yaml:"productOwner,omitempty" json:"productOwner,omitempty"` 22 | // A list of users which constitute this owner 23 | Members []string `yaml:"members,omitempty" json:"members,omitempty"` 24 | // Map of string (group name e.g. some-owner) of strings (list of usernames), one username for each group is required. 25 | Groups map[string][]string `yaml:"groups,omitempty" json:"groups,omitempty"` 26 | // A list of users that are allowed to promote services in this owner space 27 | Promoters []string `yaml:"promoters,omitempty" json:"promoters,omitempty"` 28 | // The default jira project that is used by this owner space 29 | DefaultJiraProject *string `yaml:"defaultJiraProject,omitempty" json:"defaultJiraProject,omitempty"` 30 | // ISO-8601 UTC date time at which this information was originally committed. When sending an update, include the original timestamp you got so we can detect concurrent updates. 31 | TimeStamp string `yaml:"-" json:"timeStamp"` 32 | // The git commit hash this information was originally committed under. When sending an update, include the original commitHash you got so we can detect concurrent updates. 33 | CommitHash string `yaml:"-" json:"commitHash"` 34 | // The jira issue to use for committing a change, or the last jira issue used. 35 | JiraIssue string `yaml:"-" json:"jiraIssue"` 36 | // A display name of the owner, to be presented in user interfaces instead of the owner's name, when available 37 | DisplayName *string `yaml:"displayName,omitempty" json:"displayName,omitempty"` 38 | Links []Link `yaml:"links,omitempty" json:"links,omitempty"` 39 | } 40 | -------------------------------------------------------------------------------- /api/generated_model_owner_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // OwnerDto struct for OwnerDto 15 | type OwnerDto struct { 16 | // The contact information of the owner 17 | Contact string `yaml:"contact" json:"contact"` 18 | // The teams channel url information of the owner 19 | TeamsChannelURL *string `yaml:"teamsChannelURL,omitempty" json:"teamsChannelURL,omitempty"` 20 | // The product owner of this owner space 21 | ProductOwner *string `yaml:"productOwner,omitempty" json:"productOwner,omitempty"` 22 | // A list of users which constitute this owner 23 | Members []string `yaml:"members,omitempty" json:"members,omitempty"` 24 | // Collection of arbitrary user groups which can be referenced in service configurations. Map of string (group name e.g. some-owner) of strings (list of usernames), one username for each group is required. 25 | Groups map[string][]string `yaml:"groups,omitempty" json:"groups,omitempty"` 26 | // A list of users that are allowed to promote services in this owner space 27 | Promoters []string `yaml:"promoters,omitempty" json:"promoters,omitempty"` 28 | // The default jira project that is used by this owner space 29 | DefaultJiraProject *string `yaml:"defaultJiraProject,omitempty" json:"defaultJiraProject,omitempty"` 30 | // ISO-8601 UTC date time at which this information was originally committed. When sending an update, include the original timestamp you got so we can detect concurrent updates. 31 | TimeStamp string `yaml:"-" json:"timeStamp"` 32 | // The git commit hash this information was originally committed under. When sending an update, include the original commitHash you got so we can detect concurrent updates. 33 | CommitHash string `yaml:"-" json:"commitHash"` 34 | // The jira issue to use for committing a change, or the last jira issue used. 35 | JiraIssue string `yaml:"-" json:"jiraIssue"` 36 | // A display name of the owner, to be presented in user interfaces instead of the owner's name, when available 37 | DisplayName *string `yaml:"displayName,omitempty" json:"displayName,omitempty"` 38 | Links []Link `yaml:"links,omitempty" json:"links,omitempty"` 39 | } 40 | -------------------------------------------------------------------------------- /test/acceptance/webhook_github_acc_test.go: -------------------------------------------------------------------------------- 1 | package acceptance 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/Interhyp/go-backend-service-common/docs" 8 | "github.com/go-http-utils/headers" 9 | "github.com/go-playground/webhooks/v6/github" 10 | "github.com/stretchr/testify/require" 11 | "net/http" 12 | "testing" 13 | ) 14 | 15 | func TestPOSTWebhookGitHub_CheckSuite_Success(t *testing.T) { 16 | tstReset() 17 | 18 | docs.When("When GitHub sends a webhook with valid payload") 19 | body := createGithubCheckSuitePayload("a800c51995d3f3ee0ca110fa5fd93a772eaff381") 20 | 21 | bodyBytes, err := json.Marshal(&body) 22 | require.Nil(t, err) 23 | request, err := http.NewRequest(http.MethodPost, ts.URL+"/webhooks/vcs/github", bytes.NewReader(bodyBytes)) 24 | require.Nil(t, err) 25 | request.Header.Set("X-GitHub-Event", string(github.CheckSuiteEvent)) 26 | request.Header.Set(headers.ContentType, "application/json") 27 | rawResponse, err := http.DefaultClient.Do(request) 28 | require.Nil(t, err) 29 | response, err := tstWebResponseFromResponse(rawResponse) 30 | require.Nil(t, err) 31 | 32 | docs.Then("Then the request is successful") 33 | tstAssertNoBody(t, response, err, http.StatusNoContent) 34 | } 35 | 36 | func TestPOSTWebhookGitHub_InvalidCheckSuitePayload(t *testing.T) { 37 | tstReset() 38 | 39 | docs.When("When they send a webhook with invalid payload") 40 | request, err := http.NewRequest(http.MethodPost, ts.URL+"/webhooks/vcs/github", bytes.NewReader([]byte(""))) 41 | require.Nil(t, err) 42 | request.Header.Set("X-GitHub-Event", string(github.CheckSuiteEvent)) 43 | request.Header.Set(headers.ContentType, "application/json") 44 | rawResponse, err := http.DefaultClient.Do(request) 45 | require.Nil(t, err) 46 | response, err := tstWebResponseFromResponse(rawResponse) 47 | require.Nil(t, err) 48 | 49 | docs.Then("Then the request fails and the error response is as expected") 50 | tstAssert(t, response, err, http.StatusBadRequest, "webhook-invalid.json") 51 | } 52 | 53 | func createGithubCheckSuitePayload(sha string) github.CheckSuitePayload { 54 | s := fmt.Sprintf(`{"action": "requested", "check_suite": {"head_sha": "%s", "app": {"created_at": "2025-04-04T00:00:00Z","updated_at": "2025-04-04T00:00:00Z"}}, "repository": {"name": "some-repo", "ssh_url": "ssh://git@github.com:Someorg/some-service-deployment.git", "owner": {"login": "some-org"}}}`, sha) 55 | data := github.CheckSuitePayload{} 56 | _ = json.Unmarshal([]byte(s), &data) 57 | return data 58 | } 59 | -------------------------------------------------------------------------------- /api/generated_model_service_create_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // ServiceCreateDto struct for ServiceCreateDto 15 | type ServiceCreateDto struct { 16 | // The alias of the service owner. Note, an update with changed owner will move the service and any associated repositories to the new owner, but of course this will not move e.g. Jenkins jobs. That's your job. 17 | Owner string `yaml:"-" json:"owner"` 18 | // A short description of the functionality of the service. 19 | Description *string `yaml:"description,omitempty" json:"description,omitempty"` 20 | // A list of quicklinks related to the service 21 | Quicklinks []Quicklink `yaml:"quicklinks" json:"quicklinks"` 22 | // The keys of repositories associated with the service. When sending an update, they must refer to repositories that belong to this service, or the update will fail 23 | Repositories []string `yaml:"repositories" json:"repositories"` 24 | // The default channel used to send any alerts of the service to. Can be an email address or a Teams webhook URL 25 | AlertTarget string `yaml:"alertTarget" json:"alertTarget"` 26 | // The operation type of the service. 'WORKLOAD' follows the default deployment strategy of one instance per environment, 'PLATFORM' one instance per cluster or node and 'APPLICATION' is a standalone application that is not deployed via the common strategies. 27 | OperationType *string `yaml:"operationType,omitempty" json:"operationType,omitempty"` 28 | // The value defines if the service is available from the internet and the time period in which security holes must be processed. 29 | InternetExposed *bool `yaml:"internetExposed,omitempty" json:"internetExposed,omitempty"` 30 | Tags []string `yaml:"tags,omitempty" json:"tags,omitempty"` 31 | Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` 32 | Spec *ServiceSpecDto `yaml:"spec,omitempty" json:"spec,omitempty"` 33 | // Post promote dependencies. 34 | PostPromotes *PostPromote `yaml:"postPromotes,omitempty" json:"postPromotes,omitempty"` 35 | // The jira issue to use for committing a change, or the last jira issue used. 36 | JiraIssue string `yaml:"-" json:"jiraIssue"` 37 | } 38 | -------------------------------------------------------------------------------- /test/acceptance/util_auth_test.go: -------------------------------------------------------------------------------- 1 | package acceptance 2 | 3 | func tstUnauthenticated() string { 4 | return "" 5 | } 6 | 7 | // We created a keyset and some tokens using jwt.io. Use this keyset ONLY for automated tests! 8 | 9 | func tstValidUserToken() string { 10 | return "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZ3JvdXBzIjpbXSwiaWF0IjoxNTE2MjM5MDIyfQ.R5LZokxTi00xyVCXQlKbtkEFjGt3ezSSS9ycEspctVeTGTCwE_XPpJqbuSyJE3_U2phuAyBpIiB0qvi4_qYNsEW6xgf2eih7uqmemzQOM8jfFw_XuYPWQJ1G0LkOZMS4-q_VYgrufOUTdpECsZD9tgZOuf-zkx30UqN3-rhac3PtOtOjpv7gl5zWS8lD-eHDW7-AgrlyGWbLGfJoahGxsb-h2QOnJDDXUA4yUKYDX4Yb_9y4Np8OObCzSm0YJJlssa54L5ruluzEG_2fXvjbUDS8FlEKUqkD337ttMld41RpKnMzfXvC_mgr1zE7loDcFXQEyHSYXma5jM617RUxbg" 11 | } 12 | 13 | func tstValidAdminToken() string { 14 | return "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZ3JvdXBzIjpbImFkbWluIl0sImlhdCI6MTUxNjIzOTAyMn0.nHGB2gk8Zyyjo5mNQm4IKF540IlbO6uR6FdzY9Oz37hGfui-pfuHAlnCoY-N1fG6xvfZe3su3EOnzOtT3BXmMEkJ3fQWexwXQKEfFDJqcAKudEvw-OTs8SO9Uc81H0d5IjGm6frJ-XKs7tSrCPGf6HC_KVx2vk19AV5eBaZNmIJTjMNBEfjgSH8lSlzpiszu-X4PYMV6j9f8PPPjdlDPMTrHx_kq1wewWhkE6TLROzdHf8w8Ip_KHRzBADdR51O_ZqONxiJMKMn7K399QpJOvEv9eI5qoYljesPwBzsFRnowNQhB3ufNWHCuCkyBM5AyW6oEo5SSkBQIry1cGUn0XA" 15 | } 16 | 17 | func tstInvalidSignatureToken() string { 18 | return "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZ3JvdXBzIjpbImFkbWluIl0sImlhdCI6MTUxNjIzOTAyMn0.nHGB2gk8Zyyjo5mNQm4IKF540IlbO6uR6FdzY9Oz37hGfui-pfuHAlnCoY-N1fG6xvfZe3su3EOnzOtT3BXmMEkJ3fQWexwXQKEfFDJqcAKudEvw-OTs8SO9Uc81H0d5IjGm6frJ-XKs7tSrCPGf6HC_KVx2vk19AV5eBaZNmIJTjMNBEfjgSH8lSlzpiszu-X4PYMV6j9f8PPPjdlDPMTrHx_kq1wewWhkE6TLROzdHf8w8Ip_KHRzBADdR51O_ZqONxiJMKMn7K399QpJOvEv9eI5qoYljesPwBzsFRnowNQhB3ufNWHCuCkyBM5AyW6oEo5SSkBQIry1cGUn0X" 19 | } 20 | 21 | func tstInvalidAlgorithmToken() string { 22 | return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZ3JvdXBzIjpbImFkbWluIl0sImlhdCI6MTUxNjIzOTAyMn0.3NlwGm6CfBG5aU7myAOP14XMFtS0W5t9a9ZaS5lLLIw" 23 | } 24 | 25 | func tstExpiredAdminToken() string { 26 | return "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZ3JvdXBzIjpbImFkbWluIl0sImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjM5MDQyfQ.UdKc1BvLRlyYMR2x6Nle_S5tU7QH2cZsf3jctbStR5mbou3Q9mJ0ijnFEMmrva-lAghLEKp56W65PbHz6fqDLZbe1MoiFWL6sRfvwLvSboFh2uyikuqBT7dmYvyuBpYrG6IW84-bGIodCTPZI9kNFT6x2q_nNsJxYQRv5uCe88TNAdv0JYUuRDpHGl1tXFRPievF84HfYYxrQqNz2SDIYtsCC5XXh26TsN2vNG_PBisj9UabeoumBPQcuPTASgRWTjpONxkH-8L_mzKubCVM62WFESv7ZVZ_V-DgzKkm9_r_7mVEePeBEZ-su0v2EItF0mjKW_zF-BFvvR0l417jig" 27 | } 28 | -------------------------------------------------------------------------------- /test/mock/cachemock/cachemock.go: -------------------------------------------------------------------------------- 1 | package ownersmock 2 | 3 | import ( 4 | "context" 5 | "github.com/Interhyp/metadata-service/api" 6 | ) 7 | 8 | type Mock struct { 9 | } 10 | 11 | func (s *Mock) IsCache() bool { 12 | return true 13 | } 14 | 15 | func (s *Mock) Setup() error { 16 | return nil 17 | } 18 | 19 | func (s *Mock) SetOwnerListTimestamp(ctx context.Context, timestamp string) error { 20 | return nil 21 | } 22 | 23 | func (s *Mock) GetOwnerListTimestamp(ctx context.Context) (string, error) { 24 | return "", nil 25 | } 26 | 27 | func (s *Mock) GetSortedOwnerAliases(ctx context.Context) ([]string, error) { 28 | return nil, nil 29 | } 30 | 31 | func (s *Mock) GetOwner(ctx context.Context, alias string) (openapi.OwnerDto, error) { 32 | if alias == "ownerWithGroup" { 33 | return openapi.OwnerDto{ 34 | Groups: map[string][]string{"someGroupName": {"username1", "username2"}}, 35 | }, nil 36 | } 37 | return openapi.OwnerDto{}, nil 38 | } 39 | 40 | func (s *Mock) PutOwner(ctx context.Context, alias string, entry openapi.OwnerDto) error { 41 | return nil 42 | } 43 | 44 | func (s *Mock) DeleteOwner(ctx context.Context, alias string) error { 45 | return nil 46 | } 47 | 48 | func (s *Mock) SetServiceListTimestamp(ctx context.Context, timestamp string) error { 49 | return nil 50 | } 51 | 52 | func (s *Mock) GetServiceListTimestamp(ctx context.Context) (string, error) { 53 | return "", nil 54 | } 55 | 56 | func (s *Mock) GetSortedServiceNames(ctx context.Context) ([]string, error) { 57 | return nil, nil 58 | } 59 | 60 | func (s *Mock) GetService(ctx context.Context, name string) (openapi.ServiceDto, error) { 61 | return openapi.ServiceDto{}, nil 62 | } 63 | 64 | func (s *Mock) PutService(ctx context.Context, name string, entry openapi.ServiceDto) error { 65 | return nil 66 | } 67 | 68 | func (s *Mock) DeleteService(ctx context.Context, name string) error { 69 | return nil 70 | } 71 | 72 | func (s *Mock) SetRepositoryListTimestamp(ctx context.Context, timestamp string) error { 73 | return nil 74 | } 75 | 76 | func (s *Mock) GetRepositoryListTimestamp(ctx context.Context) (string, error) { 77 | return "", nil 78 | } 79 | 80 | func (s *Mock) GetSortedRepositoryKeys(ctx context.Context) ([]string, error) { 81 | return nil, nil 82 | } 83 | 84 | func (s *Mock) GetRepository(ctx context.Context, key string) (openapi.RepositoryDto, error) { 85 | return openapi.RepositoryDto{}, nil 86 | } 87 | 88 | func (s *Mock) PutRepository(ctx context.Context, key string, entry openapi.RepositoryDto) error { 89 | return nil 90 | } 91 | 92 | func (s *Mock) DeleteRepository(ctx context.Context, key string) error { 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /.github/workflows/go.yaml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**.md' 7 | 8 | jobs: 9 | build: 10 | name: 📦 Build & Test 11 | runs-on: ubuntu-latest 12 | outputs: 13 | image: ${{ steps.meta.outputs.tags }} 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: docker/setup-buildx-action@v2 17 | - uses: docker/login-action@v2 18 | with: 19 | registry: ghcr.io 20 | username: ${{ github.actor }} 21 | password: ${{ secrets.GITHUB_TOKEN }} 22 | - id: meta 23 | uses: docker/metadata-action@v5 24 | with: 25 | images: ghcr.io/${{ github.repository }} 26 | tags: | 27 | type=ref,event=branch,pattern=snapshot-{{sha}} 28 | labels: | 29 | org.opencontainers.image.source=git@github.com:${{ github.repository }}.git 30 | org.opencontainers.image.version=${{ github.head_ref || github.ref_name }} 31 | org.opencontainers.image.revision=${{ github.sha }} 32 | de.interhyp.image.servicename=metadata-service 33 | - id: build-push 34 | uses: docker/build-push-action@v5 35 | with: 36 | context: . 37 | push: true 38 | tags: ${{ steps.meta.outputs.tags }} 39 | labels: ${{ steps.meta.outputs.labels }} 40 | 41 | release: 42 | name: 🚀 Release 43 | if: github.ref == 'refs/heads/main' 44 | needs: build 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v3 48 | - uses: docker/login-action@v2 49 | with: 50 | registry: ghcr.io 51 | username: ${{ github.actor }} 52 | password: ${{ secrets.GITHUB_TOKEN }} 53 | - id: semantic-release 54 | uses: go-semantic-release/action@v1 55 | with: 56 | github-token: ${{ secrets.GITHUB_TOKEN }} 57 | prerelease: false 58 | allow-initial-development-versions: true # remove to trigger an initial 1.0.0 release 59 | changelog-generator-opt: "emojis=true" 60 | hooks: goreleaser 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | - id: repository-id 64 | uses: ASzc/change-string-case-action@v6 65 | with: 66 | string: ${{ github.repository }} 67 | - if: steps.semantic-release.outputs.version != '' 68 | run: | 69 | TARGET=ghcr.io/${{ steps.repository-id.outputs.lowercase }}:${{ steps.semantic-release.outputs.version }} 70 | SOURCE=${{ needs.build.outputs.image }} 71 | 72 | docker pull $SOURCE 73 | docker tag $SOURCE $TARGET 74 | docker push $TARGET 75 | -------------------------------------------------------------------------------- /local-config.template.yaml: -------------------------------------------------------------------------------- 1 | APPLICATION_NAME: metadata 2 | 3 | # switch off ECS json logging in favor of readable localhost logs 4 | LOGSTYLE: plain 5 | 6 | #BASIC_AUTH_USERNAME: 7 | #BASIC_AUTH_PASSWORD: 8 | 9 | # Url to this service, used as link in Pull Request validation builds 10 | PULL_REQUEST_BUILD_URL: https://metadata-service.example.com 11 | 12 | GIT_COMMITTER_NAME: 13 | GIT_COMMITTER_EMAIL: 14 | 15 | #VAULT_ENABLED: false 16 | VAULT_SERVER: some-vault.de 17 | VAULT_AUTH_TOKEN: 18 | VAULT_SECRETS_CONFIG: >- 19 | { 20 | "some/path/to/secrets": [ 21 | {"vaultKey": "BASIC_AUTH_USERNAME"}, 22 | {"vaultKey": "BASIC_AUTH_PASSWORD"}, 23 | {"vaultKey": "KAFKA_PASSWORD"}, 24 | {"vaultKey": "METADATA_CHANGE_EVENTS_CONNECTION_STRING"}, 25 | {"vaultKey": "GITHUB_APP_JWT_SIGNING_KEY_PEM"} 26 | ] 27 | } 28 | 29 | GITHUB_APP_ID: 30 | GITHUB_APP_INSTALLATION_ID: 31 | 32 | WEBHOOKS_PROCESS_ASYNC: false 33 | 34 | AUTH_OIDC_KEY_SET_URL: https://login.microsoftonline.com//discovery/v2.0/keys 35 | AUTH_OIDC_TOKEN_AUDIENCE: 36 | 37 | METADATA_REPO_URL: https://github.com/Interhyp/service-metadata-example 38 | SSH_METADATA_REPO_URL: ssh://git@github.com/Interhyp/service-metadata-example.git 39 | 40 | UPDATE_JOB_INTERVAL_MINUTES: 15 41 | UPDATE_JOB_TIMEOUT_SECONDS: 30 42 | 43 | ALERT_TARGET_REGEX: '(^https://domain[.]com/)|(@domain[.]com$)' 44 | 45 | OWNER_ALIAS_FILTER_REGEX: '.*' 46 | 47 | # The NOTIFICATION_CONSUMER_CONFIGS env below is an example: 48 | 49 | #NOTIFICATION_CONSUMER_CONFIGS: >- 50 | # { 51 | # "consumerName": { 52 | # "types": { 53 | # "Owner": ["CREATED", "MODIFIED", "DELETED"], 54 | # "Service": ["CREATED", "MODIFIED", "DELETED"], 55 | # "Repository": ["DELETED"] 56 | # }, 57 | # "url": "https://some.url.com/for/the/webhook" 58 | # }, 59 | # "anotherConsumer": { 60 | # "types": { 61 | # "Owner": ["MODIFIED"], 62 | # }, 63 | # "url": "https://another.url.com/for/another/webhook" 64 | # } 65 | # } 66 | 67 | # Enable KAFKA communication (Azure event hub example) 68 | 69 | #KAFKA_TOPICS_CONFIG: >- 70 | # { 71 | # "metadata-change-events": { 72 | # "topic": "metadata-change-events", 73 | # "brokers": [ 74 | # "example.com:9093" 75 | # ], 76 | # "username": "$ConnectionString", 77 | # "passwordEnvVar": "METADATA_CHANGE_EVENTS_CONNECTION_STRING", 78 | # "authType": "PLAIN" 79 | # } 80 | # } 81 | 82 | -------------------------------------------------------------------------------- /internal/acorn/service/mapperint.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "github.com/Interhyp/metadata-service/api" 6 | "github.com/Interhyp/metadata-service/internal/acorn/repository" 7 | ) 8 | 9 | // Mapper translates between the git repo representation (yaml) and the business entities. 10 | // 11 | // It also performs commit workflows for the metadata repository. 12 | // 13 | // It also performs the mapping between commit info and kafka messages for newly pulled commits 14 | // (because this needs knowledge of the internal commit info structures). 15 | // 16 | // Note that you are expected to hold the lock in Updater when you call any of this, so 17 | // concurrent updates of the local git tree are avoided. 18 | // 19 | // Anyway, Updater should be the only one making calls here, so this should just work. 20 | type Mapper interface { 21 | IsMapper() bool 22 | 23 | Setup() error 24 | 25 | RefreshMetadata(ctx context.Context) ([]repository.UpdateEvent, error) 26 | ContainsNewInformation(ctx context.Context, event repository.UpdateEvent) bool 27 | 28 | GetSortedOwnerAliases(ctx context.Context) ([]string, error) 29 | GetOwner(ctx context.Context, ownerAlias string) (openapi.OwnerDto, error) 30 | WriteOwner(ctx context.Context, ownerAlias string, owner openapi.OwnerDto) (openapi.OwnerDto, error) 31 | DeleteOwner(ctx context.Context, ownerAlias string, jiraIssue string) (openapi.OwnerPatchDto, error) 32 | IsOwnerEmpty(ctx context.Context, ownerAlias string) bool 33 | 34 | GetSortedServiceNames(ctx context.Context) ([]string, error) 35 | GetService(ctx context.Context, serviceName string) (openapi.ServiceDto, error) 36 | WriteService(ctx context.Context, serviceName string, service openapi.ServiceDto) (openapi.ServiceDto, error) 37 | DeleteService(ctx context.Context, serviceName string, jiraIssue string) (openapi.ServicePatchDto, error) 38 | 39 | GetSortedRepositoryKeys(ctx context.Context) ([]string, error) 40 | GetRepository(ctx context.Context, repoKey string) (openapi.RepositoryDto, error) 41 | WriteRepository(ctx context.Context, repoKey string, repository openapi.RepositoryDto) (openapi.RepositoryDto, error) 42 | DeleteRepository(ctx context.Context, repoKey string, jiraIssue string) (openapi.RepositoryPatchDto, error) 43 | 44 | // WriteServiceWithChangedOwner groups the whole operation into a single commit. 45 | // 46 | // A service takes all its referenced repositories along, but unreferenced repositories will be missed and stay. 47 | // They can be moved as part of a repository update. 48 | WriteServiceWithChangedOwner(ctx context.Context, serviceName string, service openapi.ServiceDto) (openapi.ServiceDto, error) 49 | 50 | // WriteRepositoryWithChangedOwner groups the whole operation into a single commit. 51 | // 52 | // Note that you MUST NOT call this for a repo that is referenced by a service (needs to be verified before 53 | // calling this). If referenced, the repo can only change owners together with the service. 54 | // Use WriteServiceWithChangedOwner. 55 | WriteRepositoryWithChangedOwner(ctx context.Context, repoKey string, repository openapi.RepositoryDto) (openapi.RepositoryDto, error) 56 | } 57 | -------------------------------------------------------------------------------- /internal/acorn/repository/metadataint.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "time" 7 | ) 8 | 9 | // CommitInfo holds information about a commit. 10 | type CommitInfo struct { 11 | CommitHash string 12 | TimeStamp time.Time 13 | Message string 14 | FilesChanged []string 15 | } 16 | 17 | // Metadata is the central singleton representing the service-metadata git repository. 18 | // 19 | // All operations are protected by a mutex, but of course this does not prevent multiple 20 | // goroutines from making changes between operations, so you will probably need a higher level 21 | // mutex to avoid inadvertently committing changes made by another goroutine. 22 | type Metadata interface { 23 | IsMetadata() bool 24 | 25 | Setup() error 26 | Teardown() 27 | 28 | // Clone performs an initial in-memory clone of the metadata repository on the mainline 29 | Clone(ctx context.Context) error 30 | 31 | // Pull updates the in-memory clone of the metadata repository on the mainline 32 | // 33 | // Any new commits that were not previously seen can now be obtained by NewPulledCommits. 34 | Pull(ctx context.Context) error 35 | 36 | // Commit performs a local add all and commit and returns the commit hash and the timestamp 37 | // 38 | // note: if this fails, the repository may be in an inconsistent state, so you should 39 | // Discard and Clone it again. 40 | Commit(ctx context.Context, message string) (CommitInfo, error) 41 | 42 | // Push sends commits from the in-memory clone to the upstream 43 | Push(ctx context.Context) error 44 | 45 | // Discard the in-memory clone (cannot fail, but will leave memory allocated until garbage collection) 46 | // 47 | // note: doing a new Clone implicitly discards 48 | Discard(ctx context.Context) 49 | 50 | // LastUpdated gives the time the git repo was last pulled (or pushed, which also ensures it is up-to-date). 51 | LastUpdated() time.Time 52 | 53 | // NewPulledCommits gives the business logic access to information about the newly pulled commits. 54 | // 55 | // The list is available until the next call to Pull, which clears it and adds any new commits. 56 | NewPulledCommits() []CommitInfo 57 | 58 | // IsCommitKnown is true if the given commit has been cloned, pulled or locally committed, meaning, 59 | // a Pull would not generate new information if this commit hash is in the pull. 60 | IsCommitKnown(hash string) bool 61 | 62 | // standard git-aware file operations on the current worktree 63 | 64 | Stat(filename string) (os.FileInfo, error) 65 | ReadDir(path string) ([]os.FileInfo, error) 66 | 67 | // ReadFile returns the contents of a file, the commit hash, timestamp and message for the last change to the file 68 | ReadFile(filename string) ([]byte, CommitInfo, error) 69 | 70 | // WriteFile creates or overwrites a file in the local copy 71 | WriteFile(filename string, contents []byte) error 72 | 73 | // DeleteFile deletes a file in the local copy 74 | DeleteFile(filename string) error 75 | 76 | // Mkdir creates a new directory (and potentially all directories leading up to it). Does nothing if already exists. 77 | MkdirAll(path string) error 78 | } 79 | -------------------------------------------------------------------------------- /test/resources/valid-config-unique.yaml: -------------------------------------------------------------------------------- 1 | APPLICATION_NAME: room-service 2 | PLATFORM: example 3 | 4 | LOGSTYLE: plain 5 | 6 | BASIC_AUTH_USERNAME: some-basic-auth-username 7 | BASIC_AUTH_PASSWORD: some-basic-auth-password 8 | 9 | REVIEWER_FALLBACK: username 10 | PULL_REQUEST_BUILD_URL: https://metadata-service.example.com 11 | 12 | GIT_COMMITTER_NAME: 'Body, Some' 13 | GIT_COMMITTER_EMAIL: 'somebody@somewhere.com' 14 | 15 | AUTH_OIDC_KEY_SET_URL: http://keyset 16 | AUTH_OIDC_TOKEN_AUDIENCE: some-audience 17 | AUTH_GROUP_WRITE: admin 18 | 19 | METADATA_REPO_URL: http://metadata 20 | 21 | UPDATE_JOB_INTERVAL_MINUTES: 5 22 | UPDATE_JOB_TIMEOUT_SECONDS: 30 23 | 24 | ALERT_TARGET_REGEX: '(^https://domain[.]com/)|(@domain[.]com$)' 25 | 26 | OWNER_ALIAS_PERMITTED_REGEX: '[a-z][0-1]+' 27 | OWNER_ALIAS_PROHIBITED_REGEX: '[a-z][0-2]+' 28 | OWNER_ALIAS_MAX_LENGTH: '1' 29 | OWNER_ALIAS_FILTER_REGEX: '[a-z][0-3]+' 30 | 31 | SERVICE_NAME_PERMITTED_REGEX: '[a-z][0-4]+' 32 | SERVICE_NAME_PROHIBITED_REGEX: '[a-z][0-5]+' 33 | SERVICE_NAME_MAX_LENGTH: '2' 34 | 35 | REPOSITORY_NAME_PERMITTED_REGEX: '[a-z][0-6]+' 36 | REPOSITORY_NAME_PROHIBITED_REGEX: '[a-z][0-7]+' 37 | REPOSITORY_NAME_MAX_LENGTH: '3' 38 | REPOSITORY_KEY_SEPARATOR: ';' 39 | REPOSITORY_TYPES: 'some-type,some-other-type' 40 | 41 | NOTIFICATION_CONSUMER_CONFIGS: "{}" 42 | GITHUB_APP_ID: 1 43 | GITHUB_APP_INSTALLATION_ID: 1 44 | ## this is a test key created solely for this purpose 45 | GITHUB_APP_JWT_SIGNING_KEY_PEM: |- 46 | -----BEGIN PRIVATE KEY----- 47 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDWyiEOZQ1CEjRL 48 | qysxSc4WMm7mNaQMndu9R45ZcmsimNAnH14J2Ooj2j5/andNBo51QiuRiJea2nZZ 49 | /SLD4pcd4lxRbDvY7QhLY0O8MnpHg3V2DnsJctkR8LOwwuHRORyjCYMripltk9Cj 50 | DeTwfU1AFuf9F2zYYbay03rWOc1exZFHC0eWEhJN9r0MVE99N0MVfGbb8l5BgfPP 51 | BQH7/B1A8AlqqaVnPwGUBa2jw78e5edsLbQAPt/3FWKbkOshE52WbkCes021bUwj 52 | 5j8wJhi4+UmrUvNvELLi4+thp1tU/xZ+Lu880xm7ajF1DKXo/CHPEQ7HDrjfwcdk 53 | 2LdmgfJTAgMBAAECggEAXQD57ks4Qe8zAL7VvYpZN8hPt9PrPGFQKDXnP/joxfrI 54 | SuBsrjPkMnEKVc6qaMpZfhGQXvx3tOA6lf2jg5FGYPTGh6UnhucgC9CoIEH1K6kS 55 | //MGOJGnx3pjvDquYBNsQHZae0yQ4d863JekFbQT8pfYjQELKuionOcwjblKoWl8 56 | YgiA496qVG18EOVnS3kHj5H1wJD2Xf3ptLKI+bjXAfXaiBn4fGdlqE4fHuZLHd8d 57 | 5lAcl5TU2s6G2KyXJyvMeD82/fUep+oTnRTHMtEqqDlFXmqKC6AIJm16t/IaGo4c 58 | Ym87dbYJwHD+0kERMpMqykre/AlmWlL2Lq0lL8WtgQKBgQDcBqK8gR3tVgChRve7 59 | cep5ocJYjm2RRBqbwzeOpM4tSnlJnlpIfGFLw3YFFGFsKja6aV7pr4LHk1EIslVo 60 | y2lbQnRIEGk0jGx9PgSp4dd5lsAnW/wBnwmEBNhEN1nL3lya2lXfKwUTyEXNyaXX 61 | vcXaiMt3fwzD/27SjvdoYMhogQKBgQD56FJTHqofl1K2I4n/nCMtGqxq2MzU1Gif 62 | h4NVxpD2Gn70P3h0MX+0M9wfgT1T7JFMsI1VRazncsLoDDsb9r5+EPOYY0+wv4Uy 63 | 83awKUazglYGEBDHHRdbDJkx3gsp583aY73yJrGGh5IcuW0UfhY32mKukgcj5uSn 64 | Wvn13uvQ0wKBgAbTSd8RHk2Lem+GVQ8ChKSLSQ0YNfvooe6tCp8pK6AqDEMlX2Wa 65 | PiZshM+5hyAk2xfDRwd2w1bPkhbz+URL8xO6pwLJR4oyxPbJorlmYRnLfGB8MQAX 66 | 3+Kxh8ft86IoXrULCtjma7zmXIv6smNT5rxVvAIT9eBqnxR3DOO3BOCBAoGAKHNi 67 | X/Hmt5ZW3QSDocw0JWjb36+X+BsplCjrKUcqz6saQY7EgIpCkXiTeMYCl0MDgdZS 68 | CittAUmiIs1YA/68dstnopLwoztc5BJkc786onPGWNTg4lnjHem8IkY+qFnNCDx8 69 | 0mVQ9uWa0OtyrI58Ki4/KuKYJUeKW0xuiU27/eECgYBZS8SpocgTeHSs6tC4mYr/ 70 | GHC84dc4JrBll9zVtW3amw5+eUU31h48mEEFM4Sph4YlMIEenNiy0+6QAr3P212B 71 | +r5dw0/D3o4wp7VYaieS11g2ZrMgLVFbKCvyH4rNdPn6QgSsxK22SnoPDkiJAbMS 72 | 0TEd3w/5KBsZU2kLdnQ0/Q== 73 | -----END PRIVATE KEY----- -------------------------------------------------------------------------------- /internal/repository/notifier/client/notifier/client.go: -------------------------------------------------------------------------------- 1 | package notifierclient 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | openapi "github.com/Interhyp/metadata-service/api" 7 | "github.com/Interhyp/metadata-service/internal/acorn/config" 8 | "net/http" 9 | "time" 10 | 11 | librepo "github.com/Interhyp/go-backend-service-common/acorns/repository" 12 | aurestbreakerprometheus "github.com/StephanHCB/go-autumn-restclient-circuitbreaker-prometheus" 13 | aurestbreaker "github.com/StephanHCB/go-autumn-restclient-circuitbreaker/implementation/breaker" 14 | aurestclientprometheus "github.com/StephanHCB/go-autumn-restclient-prometheus" 15 | aurestclientapi "github.com/StephanHCB/go-autumn-restclient/api" 16 | auresthttpclient "github.com/StephanHCB/go-autumn-restclient/implementation/httpclient" 17 | aurestlogging "github.com/StephanHCB/go-autumn-restclient/implementation/requestlogging" 18 | ) 19 | 20 | type NotifierClient interface { 21 | Setup(clientIdentifier string, url string) error 22 | 23 | // Send will log any errors, but since we use it async, it cannot return the error 24 | Send(ctx context.Context, notification openapi.Notification) 25 | } 26 | 27 | type Impl struct { 28 | Logging librepo.Logging 29 | CustomConfiguration config.CustomConfiguration 30 | 31 | clientIdentifier string 32 | url string 33 | 34 | Client aurestclientapi.Client 35 | } 36 | 37 | func New(logging librepo.Logging, configuration config.CustomConfiguration) NotifierClient { 38 | return &Impl{ 39 | Logging: logging, 40 | CustomConfiguration: configuration, 41 | } 42 | } 43 | 44 | func (i *Impl) Setup(clientIdentifier string, url string) error { 45 | i.clientIdentifier = clientIdentifier 46 | i.url = url 47 | 48 | client, err := auresthttpclient.New(0, nil, nil) 49 | if err != nil { 50 | return err 51 | } 52 | aurestclientprometheus.InstrumentHttpClient(client) 53 | 54 | logWrapper := aurestlogging.New(client) 55 | 56 | circuitBreakerWrapper := aurestbreaker.New( 57 | logWrapper, 58 | fmt.Sprintf("notifier-%s-client", clientIdentifier), 59 | 100, 60 | 5*time.Minute, 61 | 60*time.Second, 62 | // includes possible retries, once the context is cancelled further requests will fail directly 63 | 15*time.Second, 64 | ) 65 | aurestbreakerprometheus.InstrumentCircuitBreakerClient(circuitBreakerWrapper) 66 | 67 | // allow tests to pre-populate 68 | if i.Client == nil { 69 | i.Client = circuitBreakerWrapper 70 | } 71 | 72 | return nil 73 | } 74 | 75 | func (i *Impl) Send(ctx context.Context, notification openapi.Notification) { 76 | var responseData *[]byte 77 | responseDto := &aurestclientapi.ParsedResponse{ 78 | Body: &responseData, 79 | } 80 | err := i.Client.Perform(ctx, http.MethodPost, i.url, notification, responseDto) 81 | if err != nil { 82 | i.Logging.Logger().Ctx(ctx).Warn().WithErr(err).Printf("failure in downstream notifier %s: %s", i.clientIdentifier, err.Error()) 83 | return 84 | } 85 | if responseData != nil { 86 | i.Logging.Logger().Ctx(ctx).Info().Printf("got response result in downstream notifier %s %s", i.clientIdentifier, string(*responseData)) 87 | } 88 | if responseDto.Status != http.StatusNoContent { 89 | i.Logging.Logger().Ctx(ctx).Warn().Printf("unexpected response status in downstream notifier %s: %d", i.clientIdentifier, responseDto.Status) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /internal/service/mapper/owner.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | "context" 5 | "github.com/Interhyp/metadata-service/api" 6 | "github.com/Interhyp/metadata-service/internal/service/util" 7 | "sort" 8 | ) 9 | 10 | func (s *Impl) GetSortedOwnerAliases(_ context.Context) ([]string, error) { 11 | fileInfos, err := s.Metadata.ReadDir("owners/") 12 | if err != nil { 13 | return []string{}, err 14 | } 15 | 16 | result := make([]string, 0) 17 | for i := range fileInfos { 18 | alias := fileInfos[i].Name() 19 | if fileInfos[i].IsDir() { 20 | // check presence of owner.info.yaml to be sure 21 | _, err := s.Metadata.Stat("owners/" + alias + "/owner.info.yaml") 22 | if err == nil { 23 | if s.CustomConfiguration.OwnerFilterAliasRegex().MatchString(alias) { 24 | result = append(result, alias) 25 | } 26 | } 27 | } 28 | } 29 | 30 | sort.Strings(result) 31 | return result, nil 32 | } 33 | 34 | func (s *Impl) GetOwner(ctx context.Context, ownerAlias string) (openapi.OwnerDto, error) { 35 | result := openapi.OwnerDto{} 36 | 37 | fullPath := "owners/" + ownerAlias + "/owner.info.yaml" 38 | err := GetT[openapi.OwnerDto](ctx, s, &result, fullPath) 39 | 40 | if nil == err { 41 | if result.Groups != nil { 42 | s.processGroupMap(ctx, result.Groups) 43 | } 44 | } 45 | 46 | return result, err 47 | } 48 | 49 | func (s *Impl) processGroupMap(ctx context.Context, groupsMap map[string][]string) { 50 | for groupName, groupMembers := range groupsMap { 51 | users, groups := util.SplitUsersAndGroups(groupMembers) 52 | if len(users) > 0 { 53 | groupsMap[groupName] = append(users, groups...) 54 | 55 | if len(groupsMap[groupName]) <= 0 && len(users) > 0 { 56 | s.Logging.Logger().Ctx(ctx).Warn().Printf("Fallback to predefined reviewers") 57 | groupsMap[groupName] = append(groupsMap[groupName], s.CustomConfiguration.ReviewerFallback()) 58 | } 59 | } 60 | } 61 | } 62 | 63 | func (s *Impl) WriteOwner(ctx context.Context, ownerAlias string, owner openapi.OwnerDto) (openapi.OwnerDto, error) { 64 | err := s.Metadata.Pull(ctx) 65 | if err != nil { 66 | return owner, err 67 | } 68 | 69 | path := "owners/" + ownerAlias 70 | fileName := "owner.info.yaml" 71 | description := "owner " + ownerAlias 72 | err = WriteT[openapi.OwnerDto](ctx, s, &owner, path, fileName, description, owner.JiraIssue) 73 | 74 | return owner, err 75 | } 76 | 77 | func (s *Impl) DeleteOwner(ctx context.Context, ownerAlias string, jiraIssue string) (openapi.OwnerPatchDto, error) { 78 | result := openapi.OwnerPatchDto{} 79 | 80 | err := s.Metadata.Pull(ctx) 81 | if err != nil { 82 | return result, err 83 | } 84 | 85 | fullPath := "owners/" + ownerAlias + "/owner.info.yaml" 86 | description := "owner " + ownerAlias 87 | err = DeleteT[openapi.OwnerPatchDto](ctx, s, &result, fullPath, description, jiraIssue) 88 | 89 | return result, err 90 | } 91 | 92 | func (s *Impl) IsOwnerEmpty(_ context.Context, ownerAlias string) bool { 93 | s.muOwnerCaches.Lock() 94 | defer s.muOwnerCaches.Unlock() 95 | 96 | for _, owner := range s.serviceOwnerCache { 97 | if owner == ownerAlias { 98 | return false 99 | } 100 | } 101 | 102 | for _, owner := range s.repositoryOwnerCache { 103 | if owner == ownerAlias { 104 | return false 105 | } 106 | } 107 | 108 | return true 109 | } 110 | -------------------------------------------------------------------------------- /api/generated_model_service_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // ServiceDto struct for ServiceDto 15 | type ServiceDto struct { 16 | // The alias of the service owner. Note, an update with changed owner will move the service and any associated repositories to the new owner, but of course this will not move e.g. Jenkins jobs. That's your job. 17 | Owner string `yaml:"-" json:"owner"` 18 | // A short description of the functionality of the service. 19 | Description *string `yaml:"description,omitempty" json:"description,omitempty"` 20 | // A list of quicklinks related to the service 21 | Quicklinks []Quicklink `yaml:"quicklinks" json:"quicklinks"` 22 | // The keys of repositories associated with the service. When sending an update, they must refer to repositories that belong to this service, or the update will fail 23 | Repositories []string `yaml:"repositories" json:"repositories"` 24 | // The default channel used to send any alerts of the service to. Can be an email address or a Teams webhook URL 25 | AlertTarget string `yaml:"alertTarget" json:"alertTarget"` 26 | // The operation type of the service. 'WORKLOAD' follows the default deployment strategy of one instance per environment, 'PLATFORM' one instance per cluster or node and 'APPLICATION' is a standalone application that is not deployed via the common strategies. 27 | OperationType *string `yaml:"operationType,omitempty" json:"operationType,omitempty"` 28 | // The value defines if the service is available from the internet and the time period in which security holes must be processed. 29 | InternetExposed *bool `yaml:"internetExposed,omitempty" json:"internetExposed,omitempty"` 30 | Tags []string `yaml:"tags,omitempty" json:"tags,omitempty"` 31 | Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` 32 | Spec *ServiceSpecDto `yaml:"spec,omitempty" json:"spec,omitempty"` 33 | // Post promote dependencies. 34 | PostPromotes *PostPromote `yaml:"postPromotes,omitempty" json:"postPromotes,omitempty"` 35 | // ISO-8601 UTC date time at which this information was originally committed. When sending an update, include the original timestamp you got so we can detect concurrent updates. 36 | TimeStamp string `yaml:"-" json:"timeStamp"` 37 | // The git commit hash this information was originally committed under. When sending an update, include the original commitHash you got so we can detect concurrent updates. 38 | CommitHash string `yaml:"-" json:"commitHash"` 39 | // The jira issue to use for committing a change, or the last jira issue used. 40 | JiraIssue string `yaml:"-" json:"jiraIssue"` 41 | // The current phase of the service's development. A service usually starts off as 'experimental', then becomes 'operational' (i. e. can be reliably used and/or consumed). Once 'deprecated', the service doesn’t guarantee reliable use/consumption any longer and if 'decommissionable', the service will soon cease to exist. 42 | Lifecycle *string `yaml:"lifecycle,omitempty" json:"lifecycle,omitempty"` 43 | } 44 | -------------------------------------------------------------------------------- /api/generated_model_service_patch_dto.go: -------------------------------------------------------------------------------- 1 | /* 2 | Metadata 3 | 4 | Obtain and manage metadata for owners, services, repositories. Please see [README](https://github.com/Interhyp/metadata-service/blob/main/README.md) for details. **CLIENTS MUST READ!** 5 | 6 | API version: v1 7 | Contact: somebody@some-organisation.com 8 | */ 9 | 10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. 11 | 12 | package openapi 13 | 14 | // ServicePatchDto struct for ServicePatchDto 15 | type ServicePatchDto struct { 16 | // The alias of the service owner. Note, a patch with changed owner will move the service and any associated repositories to the new owner, but of course this will not move e.g. Jenkins jobs. That's your job. 17 | Owner *string `yaml:"owner,omitempty" json:"owner,omitempty"` 18 | // A short description of the functionality of the service. 19 | Description *string `yaml:"description,omitempty" json:"description,omitempty"` 20 | // A list of quicklinks related to the service 21 | Quicklinks []Quicklink `yaml:"quicklinks,omitempty" json:"quicklinks,omitempty"` 22 | // The keys of repositories associated with the service. When sending an update, they must refer to repositories that belong to this service, or the update will fail 23 | Repositories []string `yaml:"repositories,omitempty" json:"repositories,omitempty"` 24 | // The default channel used to send any alerts of the service to. Can be an email address or a Teams webhook URL 25 | AlertTarget *string `yaml:"alertTarget,omitempty" json:"alertTarget,omitempty"` 26 | // The operation type of the service. 'WORKLOAD' follows the default deployment strategy of one instance per environment, 'PLATFORM' one instance per cluster or node and 'APPLICATION' is a standalone application that is not deployed via the common strategies. 27 | OperationType *string `yaml:"operationType,omitempty" json:"operationType,omitempty"` 28 | // The value defines if the service is available from the internet and the time period in which security holes must be processed. 29 | InternetExposed *bool `yaml:"internetExposed,omitempty" json:"internetExposed,omitempty"` 30 | Tags []string `yaml:"tags,omitempty" json:"tags,omitempty"` 31 | Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` 32 | Spec *ServiceSpecDto `yaml:"spec,omitempty" json:"spec,omitempty"` 33 | // Post promote dependencies. 34 | PostPromotes *PostPromote `yaml:"postPromotes,omitempty" json:"postPromotes,omitempty"` 35 | // ISO-8601 UTC date time at which this information was originally committed. When sending an update, include the original timestamp you got so we can detect concurrent updates. 36 | TimeStamp string `yaml:"-" json:"timeStamp"` 37 | // The git commit hash this information was originally committed under. When sending an update, include the original commitHash you got so we can detect concurrent updates. 38 | CommitHash string `yaml:"-" json:"commitHash"` 39 | // The jira issue to use for committing a change, or the last jira issue used. 40 | JiraIssue string `yaml:"-" json:"jiraIssue"` 41 | // The current phase of the service's development. A service usually starts off as 'experimental', then becomes 'operational' (i. e. can be reliably used and/or consumed). Once 'deprecated', the service doesn’t guarantee reliable use/consumption any longer. 42 | Lifecycle *string `yaml:"lifecycle,omitempty" json:"lifecycle,omitempty"` 43 | } 44 | -------------------------------------------------------------------------------- /internal/web/util/responsehelper.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/Interhyp/go-backend-service-common/web/util/media" 7 | "github.com/Interhyp/metadata-service/api" 8 | aulogging "github.com/StephanHCB/go-autumn-logging" 9 | "github.com/go-http-utils/headers" 10 | "net/http" 11 | "time" 12 | ) 13 | 14 | func Success(ctx context.Context, w http.ResponseWriter, _ *http.Request, response interface{}) { 15 | w.Header().Set(headers.ContentType, media.ContentTypeApplicationJson) 16 | WriteJson(ctx, w, response) 17 | } 18 | 19 | func SuccessWithStatus(ctx context.Context, w http.ResponseWriter, _ *http.Request, response interface{}, status int) { 20 | w.Header().Set(headers.ContentType, media.ContentTypeApplicationJson) 21 | w.WriteHeader(status) 22 | WriteJson(ctx, w, response) 23 | } 24 | 25 | func SuccessNoBody(ctx context.Context, w http.ResponseWriter, _ *http.Request, status int) { 26 | w.WriteHeader(status) 27 | } 28 | 29 | func UnexpectedErrorHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, err error, timeStamp time.Time) { 30 | aulogging.Logger.Ctx(ctx).Error().WithErr(err).Printf("unexpected error") 31 | ErrorHandler(ctx, w, r, "unknown", http.StatusInternalServerError, err.Error(), timeStamp) 32 | } 33 | 34 | func BadGatewayErrorHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, err error, timeStamp time.Time) { 35 | aulogging.Logger.Ctx(ctx).Error().WithErr(err).Printf("bad gateway") 36 | ErrorHandler(ctx, w, r, "downstream.unavailable", http.StatusBadGateway, "the git server is currently unavailable or failed to service the request", timeStamp) 37 | } 38 | 39 | func UnauthorizedErrorHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, logMessage string, timeStamp time.Time) { 40 | aulogging.Logger.Ctx(ctx).Info().Printf("unauthorized: %s", logMessage) 41 | ErrorHandler(ctx, w, r, "unauthorized", http.StatusUnauthorized, "missing or invalid Authorization header (JWT bearer token expected) or token invalid or expired", timeStamp) 42 | } 43 | 44 | func ForbiddenErrorHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, logMessage string, timeStamp time.Time) { 45 | aulogging.Logger.Ctx(ctx).Info().Printf("forbidden: %s", logMessage) 46 | ErrorHandler(ctx, w, r, "forbidden", http.StatusForbidden, "you are not authorized for this operation", timeStamp) 47 | } 48 | 49 | func DeletionBodyInvalid(ctx context.Context, w http.ResponseWriter, r *http.Request, err error, timeStamp time.Time) { 50 | aulogging.Logger.Ctx(ctx).Info().Printf("deletion body invalid: %s", err.Error()) 51 | ErrorHandler(ctx, w, r, "deletion.invalid.body", http.StatusBadRequest, "body failed to parse", timeStamp) 52 | } 53 | 54 | func ErrorHandler(ctx context.Context, w http.ResponseWriter, _ *http.Request, msg string, status int, details string, timestamp time.Time) { 55 | detailsPtr := &details 56 | if details == "" { 57 | detailsPtr = nil 58 | } 59 | response := &openapi.ErrorDto{ 60 | Details: detailsPtr, 61 | Message: &msg, 62 | Timestamp: ×tamp, 63 | } 64 | w.Header().Set(headers.ContentType, media.ContentTypeApplicationJson) 65 | w.WriteHeader(status) 66 | WriteJson(ctx, w, response) 67 | } 68 | 69 | func WriteJson(ctx context.Context, w http.ResponseWriter, v interface{}) { 70 | encoder := json.NewEncoder(w) 71 | encoder.SetEscapeHTML(false) 72 | err := encoder.Encode(v) 73 | if err != nil { 74 | aulogging.Logger.Ctx(ctx).Warn().WithErr(err).Printf("error while encoding json response: %v", err) 75 | // can't change status anymore, in the middle of the response now 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /internal/repository/github/github.go: -------------------------------------------------------------------------------- 1 | package githubclient 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | librepo "github.com/Interhyp/go-backend-service-common/acorns/repository" 8 | "github.com/Interhyp/metadata-service/internal/acorn/repository" 9 | "github.com/google/go-github/v70/github" 10 | ) 11 | 12 | type Impl struct { 13 | client *github.Client 14 | Timestamp librepo.Timestamp 15 | } 16 | 17 | func New(timestamp librepo.Timestamp, client *github.Client) *Impl { 18 | return &Impl{ 19 | Timestamp: timestamp, 20 | client: client, 21 | } 22 | } 23 | 24 | func (r *Impl) StartCheckRun(ctx context.Context, owner, repoName, checkName, sha string) (int64, error) { 25 | result, _, err := r.client.Checks.CreateCheckRun(ctx, owner, repoName, github.CreateCheckRunOptions{ 26 | Name: checkName, 27 | HeadSHA: sha, 28 | StartedAt: &github.Timestamp{ 29 | Time: r.Timestamp.Now(), 30 | }, 31 | Status: github.Ptr("in_progress"), 32 | }) 33 | if err != nil { 34 | return -1, err 35 | } 36 | if result.ID == nil { 37 | return -1, fmt.Errorf("creating check run '%s' for %s/%s @ %s returned no id", checkName, owner, repoName, sha) 38 | } 39 | return result.GetID(), err 40 | } 41 | 42 | func (r *Impl) ConcludeCheckRun(ctx context.Context, owner, repoName, checkName string, checkRunId int64, conclusion repository.CheckRunConclusion, output github.CheckRunOutput, actions ...*github.CheckRunAction) error { 43 | annotationLimit := 50 44 | annotations := output.Annotations 45 | errs := make([]error, 0) 46 | for len(annotations) > annotationLimit { 47 | batch := annotations[:annotationLimit] 48 | annotations = annotations[annotationLimit:] 49 | _, _, err := r.client.Checks.UpdateCheckRun(ctx, owner, repoName, checkRunId, github.UpdateCheckRunOptions{ 50 | Name: checkName, 51 | Status: github.Ptr("in_progress"), 52 | Output: &github.CheckRunOutput{ 53 | Title: output.Title, 54 | Summary: output.Summary, 55 | Annotations: batch, 56 | }, 57 | }) 58 | errs = append(errs, err) 59 | } 60 | text := output.Text 61 | if text != nil { 62 | runes := []rune(*text) 63 | // If body is longer than 65535 chars, Github returns 422 Invalid request with message "Only 65535 characters are allowed; 79127 were supplied." 64 | ghCharLimit := 65535 65 | if len(runes) > ghCharLimit { 66 | warning := []rune("# :warning: Too many errors for one message. Fix issues below and run check again against fixed commit.\n") 67 | maxLength := ghCharLimit - len(warning) 68 | updated := string(append(warning, runes[:maxLength]...)) 69 | text = &updated 70 | } 71 | } 72 | _, _, err := r.client.Checks.UpdateCheckRun(ctx, owner, repoName, checkRunId, github.UpdateCheckRunOptions{ 73 | Name: checkName, 74 | Status: github.Ptr("completed"), 75 | Conclusion: github.Ptr(string(conclusion)), 76 | CompletedAt: &github.Timestamp{ 77 | Time: r.Timestamp.Now(), 78 | }, 79 | Output: &github.CheckRunOutput{ 80 | Title: output.Title, 81 | Summary: output.Summary, 82 | Text: text, 83 | Annotations: annotations, 84 | Images: output.Images, 85 | }, 86 | Actions: actions, 87 | }) 88 | errs = append(errs, err) 89 | return errors.Join(errs...) 90 | } 91 | 92 | func (r *Impl) GetUser(ctx context.Context, username string) (*github.User, error) { 93 | user, _, err := r.client.Users.Get(ctx, username) 94 | return user, err 95 | } 96 | 97 | func (r *Impl) CreateInstallationToken(ctx context.Context, installationId int64) (*github.InstallationToken, *github.Response, error) { 98 | return r.client.Apps.CreateInstallationToken(ctx, installationId, nil) 99 | } 100 | -------------------------------------------------------------------------------- /internal/repository/authProvider/authProvider.go: -------------------------------------------------------------------------------- 1 | package authProvider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/Interhyp/metadata-service/internal/acorn/config" 7 | "github.com/Interhyp/metadata-service/internal/acorn/repository" 8 | githubclient "github.com/Interhyp/metadata-service/internal/repository/github" 9 | auzerolog "github.com/StephanHCB/go-autumn-logging-zerolog" 10 | "github.com/bradleyfalzon/ghinstallation/v2" 11 | "github.com/go-git/go-git/v5/plumbing/transport" 12 | ghhttp "github.com/go-git/go-git/v5/plumbing/transport/http" 13 | "github.com/gofri/go-github-pagination/githubpagination" 14 | "net/http" 15 | "time" 16 | 17 | librepo "github.com/Interhyp/go-backend-service-common/acorns/repository" 18 | aulogging "github.com/StephanHCB/go-autumn-logging" 19 | "github.com/google/go-github/v70/github" 20 | ) 21 | 22 | type AuthProviderFn func(context.Context) (transport.AuthMethod, error) 23 | 24 | type Github interface { 25 | CreateInstallationToken(ctx context.Context, installationId int64) (*github.InstallationToken, *github.Response, error) 26 | } 27 | type AuthProviderImpl struct { 28 | Configuration librepo.Configuration 29 | Logging librepo.Logging 30 | 31 | CustomConfiguration config.CustomConfiguration 32 | 33 | Github Github 34 | 35 | token *github.InstallationToken 36 | authProviderFn AuthProviderFn 37 | } 38 | 39 | func New( 40 | configuration librepo.Configuration, 41 | customConfig config.CustomConfiguration, 42 | logging librepo.Logging, 43 | baseRT http.RoundTripper, 44 | ) (repository.AuthProvider, error) { 45 | jwtTransport, err := ghinstallation.NewAppsTransport(baseRT, customConfig.GithubAppId(), customConfig.GithubAppJwtSigningKeyPEM()) 46 | paginator := githubpagination.NewClient(jwtTransport, 47 | githubpagination.WithPerPage(100), 48 | githubpagination.WithMaxNumOfPages(10), 49 | ) 50 | githubClient := githubclient.New(nil, github.NewClient(paginator)) 51 | 52 | return &AuthProviderImpl{ 53 | Configuration: configuration, 54 | CustomConfiguration: customConfig, 55 | Logging: logging, 56 | Github: githubClient, 57 | }, err 58 | } 59 | 60 | func (s *AuthProviderImpl) IsAuthProvider() bool { 61 | return true 62 | } 63 | 64 | func (s *AuthProviderImpl) Setup() error { 65 | ctx := auzerolog.AddLoggerToCtx(context.Background()) 66 | 67 | if err := s.SetupProvider(ctx); err != nil { 68 | s.Logging.Logger().Ctx(ctx).Error().WithErr(err).Print("failed to set up business layer AuthProvider. BAILING OUT") 69 | return err 70 | } 71 | 72 | s.Logging.Logger().Ctx(ctx).Info().Print("successfully set up AuthProvider service") 73 | return nil 74 | } 75 | 76 | func (s *AuthProviderImpl) SetupProvider(_ context.Context) error { 77 | s.authProviderFn = s.GetAuth 78 | return nil 79 | } 80 | 81 | func (s *AuthProviderImpl) ProvideAuth(ctx context.Context) transport.AuthMethod { 82 | auth, _ := s.authProviderFn(ctx) 83 | s.Logging.Logger().Ctx(ctx).Trace().Print("using basic auth for github") 84 | return auth 85 | } 86 | 87 | // AuthProvider for a business method 88 | func (s *AuthProviderImpl) GetAuth(ctx context.Context) (transport.AuthMethod, error) { 89 | if s.token == nil || s.token.GetExpiresAt().Before(time.Now().Add(-30*time.Second)) { 90 | var err error 91 | aulogging.Logger.Ctx(ctx).Trace().Print("creat new installation token for org") 92 | s.token, _, err = s.Github.CreateInstallationToken(ctx, s.CustomConfiguration.GithubAppInstallationId()) 93 | if err != nil { 94 | return nil, fmt.Errorf("failed to create installation token: %w", err) 95 | } 96 | } 97 | 98 | return &ghhttp.BasicAuth{ 99 | Username: "x-access-token", 100 | Password: s.token.GetToken(), 101 | }, nil 102 | } 103 | -------------------------------------------------------------------------------- /internal/acorn/service/updaterint.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "github.com/Interhyp/metadata-service/api" 6 | ) 7 | 8 | // Updater is the central orchestrator component that manages information flow. 9 | type Updater interface { 10 | IsUpdater() bool 11 | 12 | Setup() error 13 | 14 | // -- Eventing -- 15 | 16 | // StartReceivingEvents starts receiving events. Called by Trigger after it has initially populated the cache. 17 | StartReceivingEvents(ctx context.Context) error 18 | 19 | // -- Locking -- 20 | 21 | // WithMetadataLock is a convenience function that will obtain the lock on the metadata repo, call 22 | // the closure, and then free the lock. 23 | // 24 | // Note that a child context (!) is passed through to your function, so other methods of Updater can know 25 | // that you are holding the lock at the moment. 26 | // 27 | // Any error closure returns is passed through, and the lock is finally released. 28 | WithMetadataLock(ctx context.Context, closure func(context.Context) error) error 29 | 30 | // -- these do lock unless used inside WithMetadataLock(), use that if you need to hold the lock longer -- 31 | 32 | // PerformFullUpdate is called by Trigger both for initial cache population and periodic updates. 33 | // 34 | // It does not send any kafka events - one situation where it might be called is when an event 35 | // has been received. 36 | // 37 | // Both the git tree and all caches are updated. 38 | PerformFullUpdate(ctx context.Context) error 39 | 40 | // PerformFullUpdateWithNotifications is called when the webhook is triggered. 41 | // 42 | // Unlike PerformFullUpdate this version sends out kafka events for any new commits. 43 | // 44 | // Both the git tree and all caches are updated. 45 | PerformFullUpdateWithNotifications(ctx context.Context) error 46 | 47 | // WriteOwner returns the owner as written, with commit hash and timestamp filled in. 48 | // 49 | // Sends a kafka event and updates the cache. 50 | WriteOwner(ctx context.Context, ownerAlias string, validOwnerDto openapi.OwnerDto) (openapi.OwnerDto, error) 51 | 52 | // DeleteOwner deletes an owner. 53 | // 54 | // Sends a kafka event and updates the cache. 55 | DeleteOwner(ctx context.Context, ownerAlias string, deletionInfo openapi.DeletionDto) error 56 | 57 | CanDeleteOwner(ctx context.Context, ownerAlias string) bool 58 | 59 | // WriteService returns the service as written, with commit hash and timestamp filled in. 60 | // 61 | // This supports changing the owner. 62 | // 63 | // Assumes up-to-date cache. 64 | // 65 | // Sends a kafka event and updates the cache. 66 | WriteService(ctx context.Context, serviceName string, validServiceDto openapi.ServiceDto) (openapi.ServiceDto, error) 67 | 68 | // DeleteService deletes a service. 69 | // 70 | // Sends a kafka event and updates the cache. 71 | DeleteService(ctx context.Context, serviceName string, deletionInfo openapi.DeletionDto) error 72 | 73 | // WriteRepository returns the repository as written, with commit hash and timestamp filled in. 74 | // 75 | // This supports changing the owner, unless the repository is referenced by a service, then you should not call this. 76 | // 77 | // Assumes up-to-date cache. 78 | // 79 | // Sends a kafka event and updates the cache. 80 | WriteRepository(ctx context.Context, key string, repository openapi.RepositoryDto) (openapi.RepositoryDto, error) 81 | 82 | // DeleteRepository deletes a repository. 83 | // 84 | // Sends a kafka event and updates the cache. 85 | DeleteRepository(ctx context.Context, key string, deletionInfo openapi.DeletionDto) error 86 | 87 | // CanMoveOrDeleteRepository checks that no service still references the repository key. 88 | // 89 | // Expects a current cache and you must be holding the lock. 90 | CanMoveOrDeleteRepository(ctx context.Context, key string) (bool, error) 91 | } 92 | -------------------------------------------------------------------------------- /internal/repository/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | librepo "github.com/Interhyp/go-backend-service-common/acorns/repository" 6 | openapi "github.com/Interhyp/metadata-service/api" 7 | "github.com/Interhyp/metadata-service/internal/acorn/config" 8 | "github.com/Interhyp/metadata-service/internal/acorn/repository" 9 | libcache "github.com/Roshick/go-autumn-synchronisation/pkg/cache" 10 | auzerolog "github.com/StephanHCB/go-autumn-logging-zerolog" 11 | "time" 12 | ) 13 | 14 | var cacheRetention = 30 * 24 * time.Hour 15 | 16 | type Impl struct { 17 | Configuration librepo.Configuration 18 | CustomConfiguration config.CustomConfiguration 19 | Logging librepo.Logging 20 | Timestamp librepo.Timestamp 21 | 22 | OwnerCache libcache.Cache[openapi.OwnerDto] 23 | ServiceCache libcache.Cache[openapi.ServiceDto] 24 | RepositoryCache libcache.Cache[openapi.RepositoryDto] 25 | TimestampCache libcache.Cache[string] 26 | } 27 | 28 | func New( 29 | configuration librepo.Configuration, 30 | customConfig config.CustomConfiguration, 31 | logging librepo.Logging, 32 | timestamp librepo.Timestamp, 33 | ) repository.Cache { 34 | return &Impl{ 35 | Configuration: configuration, 36 | CustomConfiguration: customConfig, 37 | Logging: logging, 38 | Timestamp: timestamp, 39 | } 40 | } 41 | 42 | func (s *Impl) IsCache() bool { 43 | return true 44 | } 45 | 46 | func (s *Impl) Setup() error { 47 | ctx := auzerolog.AddLoggerToCtx(context.Background()) 48 | 49 | if err := s.SetupCache(ctx); err != nil { 50 | s.Logging.Logger().Ctx(ctx).Error().WithErr(err).Print("failed to set up business layer cache. BAILING OUT") 51 | return err 52 | } 53 | 54 | s.Logging.Logger().Ctx(ctx).Info().Print("successfully set up cache") 55 | return nil 56 | } 57 | 58 | const ( 59 | ownerKeyPrefix = "v1-owner" 60 | serviceKeyPrefix = "v1-service" 61 | repositoryKeyPrefix = "v1-repository" 62 | timestampKeyPrefix = "v1-timestamp" 63 | ) 64 | 65 | func (s *Impl) SetupCache(ctx context.Context) error { 66 | redisUrl := s.CustomConfiguration.RedisUrl() 67 | if redisUrl == "" { 68 | s.Logging.Logger().Ctx(ctx).Info().Print("using in-memory cache") 69 | if s.OwnerCache == nil { 70 | s.OwnerCache = libcache.NewMemoryCache[openapi.OwnerDto]() 71 | } 72 | if s.ServiceCache == nil { 73 | s.ServiceCache = libcache.NewMemoryCache[openapi.ServiceDto]() 74 | } 75 | if s.RepositoryCache == nil { 76 | s.RepositoryCache = libcache.NewMemoryCache[openapi.RepositoryDto]() 77 | } 78 | if s.TimestampCache == nil { 79 | s.TimestampCache = libcache.NewMemoryCache[string]() 80 | } 81 | } else { 82 | s.Logging.Logger().Ctx(ctx).Info().Printf("using redis at %s", redisUrl) 83 | redisPassword := s.CustomConfiguration.RedisUrl() 84 | if s.OwnerCache == nil { 85 | cache, err := libcache.NewRedisCache[openapi.OwnerDto](redisUrl, redisPassword, ownerKeyPrefix) 86 | if err != nil { 87 | return err 88 | } 89 | s.OwnerCache = cache 90 | } 91 | if s.ServiceCache == nil { 92 | cache, err := libcache.NewRedisCache[openapi.ServiceDto](redisUrl, redisPassword, serviceKeyPrefix) 93 | if err != nil { 94 | return err 95 | } 96 | s.ServiceCache = cache 97 | } 98 | if s.RepositoryCache == nil { 99 | cache, err := libcache.NewRedisCache[openapi.RepositoryDto](redisUrl, redisPassword, repositoryKeyPrefix) 100 | if err != nil { 101 | return err 102 | } 103 | s.RepositoryCache = cache 104 | } 105 | if s.TimestampCache == nil { 106 | cache, err := libcache.NewRedisCache[string](redisUrl, redisPassword, timestampKeyPrefix) 107 | if err != nil { 108 | return err 109 | } 110 | s.TimestampCache = cache 111 | } 112 | } 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /test/resources/recordings/github/request_get_repos-some-org-some-repo-contents-owners-test-owner-dev-owner.info.yaml_8a051734.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "GET", 3 | "requestUrl": "https://api.github.com/repos/interhyp-intern-test/service-metadata/contents/owners/techex/owner.info.yaml?per_page=100\u0026ref=a800c51995d3f3ee0ca110fa5fd93a772eaff381", 4 | "requestBody": "", 5 | "parsedResponse": { 6 | "Body": "{\n \"name\": \"owner.info.yaml\",\n \"path\": \"owners-test/owner-dev/owner.info.yaml\",\n \"sha\": \"b30837091b73ab2e50c540bf7aa22c6432befc29\",\n \"size\": 479,\n \"type\": \"file\",\n \"content\": \"Y29udGFjdDogc29tZS1tYWlsQGNvbXBhbnkuY29tCnRlYW1zQ2hhbm5lbFVSTDogaHR0cHM6Ly90ZWFtcy5taWNyb3NvZnQuY29tL2wvY2hhbm5lbC8wMDAwMDAwMDAwMDAwCnByb2R1Y3RPd25lcjogcHJvZHVjdE93bmVyCmdyb3VwczoKICB1c2VyczoKICAgIC0gZmlyc3QKICAgIC0gc2Vjb25kCiAgICAtIHRoaXJkCiAgICAtIGZvdXJ0aAogICAgLSBmaWZ0aApkZWZhdWx0SmlyYVByb2plY3Q6IFJFTFRFQwptZW1iZXJzOgogIC0gZmlyc3QKICAtIHNlY29uZAogIC0gdGhpcmQKICAtIGZvdXJ0aAo=\\n\",\n \"encoding\": \"base64\"\n}", 7 | "Status": 200, 8 | "Header": { 9 | "Access-Control-Allow-Origin": [ 10 | "*" 11 | ], 12 | "Access-Control-Expose-Headers": [ 13 | "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset" 14 | ], 15 | "Cache-Control": [ 16 | "private, max-age=60, s-maxage=60" 17 | ], 18 | "Content-Security-Policy": [ 19 | "default-src 'none'" 20 | ], 21 | "Content-Type": [ 22 | "application/json; charset=utf-8" 23 | ], 24 | "Date": [ 25 | "Tue, 25 Feb 2025 13:44:39 GMT" 26 | ], 27 | "Etag": [ 28 | "W/\"b30837091b73ab2e50c540bf7aa22c6432befc29\"" 29 | ], 30 | "Last-Modified": [ 31 | "Tue, 25 Feb 2025 13:19:31 GMT" 32 | ], 33 | "Referrer-Policy": [ 34 | "origin-when-cross-origin, strict-origin-when-cross-origin" 35 | ], 36 | "Server": [ 37 | "github.com" 38 | ], 39 | "Strict-Transport-Security": [ 40 | "max-age=31536000; includeSubdomains; preload" 41 | ], 42 | "Vary": [ 43 | "Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With" 44 | ], 45 | "X-Accepted-Github-Permissions": [ 46 | "contents=read" 47 | ], 48 | "X-Content-Type-Options": [ 49 | "nosniff" 50 | ], 51 | "X-Frame-Options": [ 52 | "deny" 53 | ], 54 | "X-Github-Api-Version-Selected": [ 55 | "2022-11-28" 56 | ], 57 | "X-Github-Media-Type": [ 58 | "github.v3; format=json" 59 | ], 60 | "X-Github-Request-Id": [ 61 | "16D9:1993E9:319BF1:32A62E:67BDC947" 62 | ], 63 | "X-Ratelimit-Limit": [ 64 | "15000" 65 | ], 66 | "X-Ratelimit-Remaining": [ 67 | "14986" 68 | ], 69 | "X-Ratelimit-Reset": [ 70 | "1740493304" 71 | ], 72 | "X-Ratelimit-Resource": [ 73 | "core" 74 | ], 75 | "X-Ratelimit-Used": [ 76 | "14" 77 | ], 78 | "X-Xss-Protection": [ 79 | "0" 80 | ] 81 | }, 82 | "Time": "2025-02-25T14:44:38.189141927+01:00" 83 | } 84 | } -------------------------------------------------------------------------------- /internal/repository/authProvider/authProvider_test.go: -------------------------------------------------------------------------------- 1 | package authProvider 2 | 3 | import ( 4 | "context" 5 | "github.com/Interhyp/metadata-service/test/mock/configmock" 6 | "github.com/Interhyp/metadata-service/test/mock/githubmock" 7 | auloggingapi "github.com/StephanHCB/go-autumn-logging/api" 8 | "github.com/go-git/go-git/v5/plumbing/transport/http" 9 | "testing" 10 | 11 | "github.com/Interhyp/go-backend-service-common/docs" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | type MockLogging struct { 16 | } 17 | 18 | func (m MockLogging) IsLogging() bool { 19 | //TODO implement me 20 | panic("implement me") 21 | } 22 | 23 | func (m MockLogging) Setup() { 24 | //TODO implement me 25 | panic("implement me") 26 | } 27 | 28 | func (m MockLogging) Logger() auloggingapi.LoggingImplementation { 29 | return MockLoggingImplementation{} 30 | } 31 | 32 | type MockLoggingImplementation struct { 33 | } 34 | 35 | func (m MockLoggingImplementation) Ctx(ctx context.Context) auloggingapi.ContextAwareLoggingImplementation { 36 | return MockContextAwareLoggingImplementation{} 37 | } 38 | 39 | func (m MockLoggingImplementation) NoCtx() auloggingapi.ContextAwareLoggingImplementation { 40 | //TODO implement me 41 | panic("implement me") 42 | } 43 | 44 | type MockContextAwareLoggingImplementation struct { 45 | } 46 | 47 | func (m MockContextAwareLoggingImplementation) Trace() auloggingapi.LeveledLoggingImplementation { 48 | return MockLeveledLoggingImplementation{} 49 | } 50 | 51 | func (m MockContextAwareLoggingImplementation) Debug() auloggingapi.LeveledLoggingImplementation { 52 | return MockLeveledLoggingImplementation{} 53 | } 54 | 55 | func (m MockContextAwareLoggingImplementation) Info() auloggingapi.LeveledLoggingImplementation { 56 | return MockLeveledLoggingImplementation{} 57 | } 58 | 59 | func (m MockContextAwareLoggingImplementation) Warn() auloggingapi.LeveledLoggingImplementation { 60 | return MockLeveledLoggingImplementation{} 61 | } 62 | 63 | func (m MockContextAwareLoggingImplementation) Error() auloggingapi.LeveledLoggingImplementation { 64 | return MockLeveledLoggingImplementation{} 65 | } 66 | 67 | func (m MockContextAwareLoggingImplementation) Fatal() auloggingapi.LeveledLoggingImplementation { 68 | return MockLeveledLoggingImplementation{} 69 | } 70 | 71 | func (m MockContextAwareLoggingImplementation) Panic() auloggingapi.LeveledLoggingImplementation { 72 | return MockLeveledLoggingImplementation{} 73 | } 74 | 75 | type MockLeveledLoggingImplementation struct { 76 | } 77 | 78 | func (m MockLeveledLoggingImplementation) WithErr(err error) auloggingapi.LeveledLoggingImplementation { 79 | return MockLeveledLoggingImplementation{} 80 | } 81 | 82 | func (m MockLeveledLoggingImplementation) With(key string, value string) auloggingapi.LeveledLoggingImplementation { 83 | return MockLeveledLoggingImplementation{} 84 | } 85 | 86 | func (m MockLeveledLoggingImplementation) Print(v ...interface{}) { 87 | } 88 | 89 | func (m MockLeveledLoggingImplementation) Printf(format string, v ...interface{}) { 90 | // do nothing 91 | } 92 | 93 | func TestProvideAuth(t *testing.T) { 94 | docs.Description("AuthProviderImpl works") 95 | 96 | authProvider := AuthProviderImpl{ 97 | CustomConfiguration: new(configmock.MockConfig), 98 | Logging: MockLogging{}, 99 | Github: new(githubmock.GitHubMock), 100 | } 101 | 102 | err := authProvider.Setup() 103 | require.Nil(t, err) 104 | 105 | require.NotNil(t, authProvider) 106 | require.Equal(t, true, authProvider.IsAuthProvider()) 107 | 108 | auth := authProvider.ProvideAuth(context.Background()) 109 | require.NotNil(t, auth) 110 | if basicAuth, ok := auth.(*http.BasicAuth); ok { 111 | dereferencedBasicAuth := *basicAuth 112 | require.IsType(t, http.BasicAuth{}, dereferencedBasicAuth) 113 | } else { 114 | t.Errorf("Object expected to be of type http.BasicAuth, but was %T", auth) 115 | } 116 | } 117 | --------------------------------------------------------------------------------