From 80395d2d6f6f5e91336e558a39729791b9d162a9 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Thu, 9 Jul 2026 15:32:10 +0200 Subject: [PATCH 01/14] (gitops): Add GitRepository OpenAPI spec and code generation Define the GitRepository API with endpoints for CRUD, sync status, and immediate sync trigger. Set up oapi-codegen configs and Makefile targets to generate types, server, and client code. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Ondra Machacek --- api/gitops/v1alpha1/openapi.yaml | 625 ++++++++++ api/gitops/v1alpha1/spec.gen.cfg | 6 + api/gitops/v1alpha1/spec.gen.go | 174 +++ api/gitops/v1alpha1/types.gen.cfg | 6 + api/gitops/v1alpha1/types.gen.go | 242 ++++ internal/gitops/api/server/server.gen.cfg | 10 + internal/gitops/api/server/server.gen.go | 1290 ++++++++++++++++++++ make/gitops.mk | 48 + pkg/gitops/client/client.gen.cfg | 9 + pkg/gitops/client/client.gen.go | 1313 +++++++++++++++++++++ 10 files changed, 3723 insertions(+) create mode 100644 api/gitops/v1alpha1/openapi.yaml create mode 100644 api/gitops/v1alpha1/spec.gen.cfg create mode 100644 api/gitops/v1alpha1/spec.gen.go create mode 100644 api/gitops/v1alpha1/types.gen.cfg create mode 100644 api/gitops/v1alpha1/types.gen.go create mode 100644 internal/gitops/api/server/server.gen.cfg create mode 100644 internal/gitops/api/server/server.gen.go create mode 100644 make/gitops.mk create mode 100644 pkg/gitops/client/client.gen.cfg create mode 100644 pkg/gitops/client/client.gen.go diff --git a/api/gitops/v1alpha1/openapi.yaml b/api/gitops/v1alpha1/openapi.yaml new file mode 100644 index 0000000..36b88e2 --- /dev/null +++ b/api/gitops/v1alpha1/openapi.yaml @@ -0,0 +1,625 @@ +openapi: 3.0.4 + +info: + title: DCM GitOps Manager API + version: v1alpha1 + description: | + The DCM GitOps Manager API provides resources for configuring Git-driven + CatalogItemInstance lifecycle management. + + ## Resources + + - **GitRepositories**: Configure connections to Git repositories containing + CatalogItemInstance YAML definitions. A separate `dcm-gitops` process polls + configured repositories and reconciles DCM state. + contact: {} + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + +servers: + - url: /api/v1alpha1 + +paths: + /git-repositories: + get: + operationId: listGitRepositories + summary: List git repositories + description: | + Retrieves a paginated list of git repository configurations. + Supports standard pagination using page tokens. + parameters: + - name: page_token + in: query + required: false + schema: + type: string + description: | + Token for retrieving the next page of results. + Obtained from the next_page_token field of a previous response. + + - name: max_page_size + in: query + required: false + schema: + type: integer + format: int32 + minimum: 1 + maximum: 1000 + default: 100 + description: | + Maximum number of items to return per page. + If not specified, defaults to 100. + + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/GitRepositoryList' + + '400': + $ref: '#/components/responses/BadRequest' + + '401': + $ref: '#/components/responses/Unauthorized' + + '403': + $ref: '#/components/responses/Forbidden' + + '500': + $ref: '#/components/responses/InternalServerError' + + post: + operationId: createGitRepository + summary: Create a git repository + description: | + Creates a new git repository configuration. + + Supports user-specified IDs via the 'id' query parameter for idempotency. + If the ID is not provided, the server will generate one. + parameters: + - name: id + in: query + required: false + schema: + type: string + pattern: '^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$' + description: | + Optional user-specified ID for the git repository. + Must follow DNS-1123 label format (lowercase alphanumeric with hyphens). + If omitted, the server generates an ID. + example: production-apps + + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GitRepository' + + responses: + '201': + description: Git repository created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GitRepository' + + '400': + description: Invalid request body or validation error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + '401': + $ref: '#/components/responses/Unauthorized' + + '403': + $ref: '#/components/responses/Forbidden' + + '409': + $ref: '#/components/responses/AlreadyExists' + + '500': + $ref: '#/components/responses/InternalServerError' + + /git-repositories/{gitRepositoryId}: + get: + operationId: getGitRepository + summary: Get a git repository + description: | + Retrieves a single git repository configuration by its ID. + parameters: + - $ref: '#/components/parameters/GitRepositoryIdPath' + + responses: + '200': + description: Git repository found + content: + application/json: + schema: + $ref: '#/components/schemas/GitRepository' + + '401': + $ref: '#/components/responses/Unauthorized' + + '403': + $ref: '#/components/responses/Forbidden' + + '404': + $ref: '#/components/responses/NotFound' + + '500': + $ref: '#/components/responses/InternalServerError' + + put: + operationId: updateGitRepository + summary: Update a git repository + description: | + Replaces a git repository configuration. + parameters: + - $ref: '#/components/parameters/GitRepositoryIdPath' + + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GitRepository' + + responses: + '200': + description: Git repository updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GitRepository' + + '400': + description: Invalid request body or validation error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + '401': + $ref: '#/components/responses/Unauthorized' + + '403': + $ref: '#/components/responses/Forbidden' + + '404': + $ref: '#/components/responses/NotFound' + + '500': + $ref: '#/components/responses/InternalServerError' + + delete: + operationId: deleteGitRepository + summary: Delete a git repository + description: | + Deletes a git repository configuration. + parameters: + - $ref: '#/components/parameters/GitRepositoryIdPath' + + responses: + '204': + description: Git repository deleted successfully + + '401': + $ref: '#/components/responses/Unauthorized' + + '403': + $ref: '#/components/responses/Forbidden' + + '404': + $ref: '#/components/responses/NotFound' + + '500': + $ref: '#/components/responses/InternalServerError' + + /git-repositories/{gitRepositoryId}/status: + get: + operationId: getGitRepositoryStatus + summary: Get git repository sync status + description: | + Retrieves the current sync status for a git repository. + parameters: + - $ref: '#/components/parameters/GitRepositoryIdPath' + + responses: + '200': + description: Sync status retrieved + content: + application/json: + schema: + $ref: '#/components/schemas/GitRepositoryStatus' + + '401': + $ref: '#/components/responses/Unauthorized' + + '403': + $ref: '#/components/responses/Forbidden' + + '404': + $ref: '#/components/responses/NotFound' + + '500': + $ref: '#/components/responses/InternalServerError' + + /git-repositories/{gitRepositoryId}:sync: + post: + operationId: syncGitRepository + summary: Trigger immediate sync + description: | + Triggers an immediate reconciliation for a git repository, + bypassing the polling interval. + parameters: + - $ref: '#/components/parameters/GitRepositoryIdPath' + + responses: + '202': + description: Sync triggered + content: + application/json: + schema: + $ref: '#/components/schemas/GitRepositoryStatus' + + '401': + $ref: '#/components/responses/Unauthorized' + + '403': + $ref: '#/components/responses/Forbidden' + + '404': + $ref: '#/components/responses/NotFound' + + '500': + $ref: '#/components/responses/InternalServerError' + +components: + parameters: + GitRepositoryIdPath: + name: gitRepositoryId + in: path + required: true + schema: + type: string + pattern: '^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$' + description: Unique identifier for the git repository + example: production-apps + + schemas: + GitRepository: + type: object + x-aep-resource: + type: gitops-manager.dcm.io/git-repository + singular: git-repository + plural: git-repositories + patterns: + - git-repositories/{git_repository_id} + required: + - api_version + - display_name + - spec + properties: + uid: + type: string + description: | + Unique identifier for the git repository. This field is output-only and + immutable after creation. The ID can be optionally specified via + query parameter on creation; if not provided, the server generates a UUID. + + Follows AEP-122 resource ID conventions. + readOnly: true + pattern: '^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$' + minLength: 1 + maxLength: 63 + example: production-apps + + api_version: + type: string + pattern: '^v[0-9]+[a-z]+[0-9]+$' + description: | + Version of the GitRepository schema (e.g., v1alpha1). + Immutable after creation. + example: v1alpha1 + + display_name: + type: string + minLength: 1 + maxLength: 63 + description: | + User-friendly display name for the git repository configuration. + example: Production Apps + + spec: + $ref: '#/components/schemas/GitRepositorySpec' + + status: + $ref: '#/components/schemas/GitRepositoryStatus' + + path: + type: string + readOnly: true + pattern: '^git-repositories/[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$' + description: | + Resource path in the format: git-repositories/{gitRepositoryId} + example: git-repositories/production-apps + + create_time: + type: string + format: date-time + readOnly: true + description: Timestamp when the resource was created (RFC 3339) + example: '2026-06-10T10:30:00Z' + + update_time: + type: string + format: date-time + readOnly: true + description: Timestamp when the resource was last modified (RFC 3339) + example: '2026-06-10T12:45:00Z' + + GitRepositorySpec: + type: object + description: | + Specification for a git repository connection. + required: + - url + properties: + url: + type: string + minLength: 1 + description: | + Git repository URL. + example: "https://git.example.com/team/dcm-apps.git" + + ref: + type: object + description: | + Git reference to watch. + properties: + branch: + type: string + default: main + description: | + Branch to watch. + example: main + + path: + type: string + default: "." + description: | + Directory within the repo. Only YAML files directly at this + path root are read (not subdirectories). + example: "apps/production/" + + interval_seconds: + type: integer + format: int32 + minimum: 10 + default: 60 + description: | + Polling interval in seconds (minimum 10s). + example: 60 + + reconciliation: + type: object + description: | + Reconciliation behavior configuration. + properties: + retry_policy: + type: object + properties: + max_retries: + type: integer + format: int32 + minimum: 0 + default: 3 + description: | + Maximum number of retries on submit failure. + example: 3 + + backoff_seconds: + type: integer + format: int32 + minimum: 1 + default: 30 + description: | + Backoff between retries in seconds. + example: 30 + + GitRepositoryStatus: + type: object + description: | + Sync status for a git repository. + properties: + sync_state: + type: string + enum: + - PENDING + - SYNCED + - IN_PROGRESS + - ERROR + description: | + Current sync state. + - PENDING: Initial state, no sync has occurred yet. + - SYNCED: DCM state matches Git at lastSyncedCommit. + - IN_PROGRESS: Lifecycle changes from Git are being applied. + - ERROR: Sync failed (auth, network, parse errors). + example: SYNCED + + last_synced_commit: + type: string + description: | + The Git commit SHA that was last successfully synced. + example: "abc123def456" + + message: + type: string + description: | + Human-readable message about the current status. + Typically contains error details when sync_state is ERROR. + example: "All instances synced successfully" + + last_sync_time: + type: string + format: date-time + description: | + Timestamp of the last sync attempt (RFC 3339). + example: '2026-06-10T12:45:00Z' + + GitRepositoryList: + type: object + required: + - results + - next_page_token + properties: + results: + type: array + description: | + Array of git repository resources. + May be empty if no results match the query. + items: + $ref: '#/components/schemas/GitRepository' + + next_page_token: + type: string + description: | + Token for retrieving the next page of results. + Empty string indicates this is the last page. + example: eyJvZmZzZXQiOjEwMH0= + + Error: + type: object + description: | + Error response following RFC 7807 Problem Details for HTTP APIs + and AEP-193 Error Responses specification. + required: + - type + - status + - title + properties: + type: + type: string + format: uri-reference + description: | + Machine-readable error code. + enum: + - INVALID_ARGUMENT + - UNAUTHENTICATED + - NOT_FOUND + - ALREADY_EXISTS + - PERMISSION_DENIED + - FAILED_PRECONDITION + - INTERNAL + - UNAVAILABLE + example: INVALID_ARGUMENT + + status: + type: integer + format: int32 + description: HTTP status code (matches the response status) + example: 400 + + title: + type: string + description: | + Short, human-readable summary of the error type. + example: Invalid request parameters + + detail: + type: string + description: | + Human-readable explanation specific to this occurrence of the error. + example: Field 'spec.url' must not be empty + + instance: + type: string + format: uri-reference + description: | + Unique identifier for this specific error occurrence. + example: 7934df3e-4b63-429b-b0f5-b8d350ec165e + + responses: + BadRequest: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + type: INVALID_ARGUMENT + status: 400 + title: Invalid request parameters + detail: Field 'spec.url' must not be empty + instance: 7934df3e-4b63-429b-b0f5-b8d350ec165e + + Unauthorized: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + type: UNAUTHENTICATED + status: 401 + title: Authentication required + detail: Valid authentication credentials are required to access this resource + instance: 6c23de2d-3a52-428a-a605-a7d240db054d + + Forbidden: + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + type: PERMISSION_DENIED + status: 403 + title: Access forbidden + detail: User does not have permission to access this resource + instance: 8a45ef4f-5c74-53ac-c1f6-c9e461fd276f + + NotFound: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + type: NOT_FOUND + status: 404 + title: Resource not found + detail: GitRepository 'production-apps' does not exist + instance: 9b56fg5g-6d85-64bd-d2g7-d0f572ge387g + + AlreadyExists: + description: Already Exists + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + type: ALREADY_EXISTS + status: 409 + title: Resource already exists + detail: GitRepository with id 'production-apps' already exists + instance: 0c67gh6h-7e96-75ce-e3h8-e1g683hf498h + + InternalServerError: + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + type: INTERNAL + status: 500 + title: Internal server error + detail: An unexpected error occurred while processing the request + instance: 2e89ij8j-9g18-97eg-g5j0-g3i805jh610j diff --git a/api/gitops/v1alpha1/spec.gen.cfg b/api/gitops/v1alpha1/spec.gen.cfg new file mode 100644 index 0000000..3bd409f --- /dev/null +++ b/api/gitops/v1alpha1/spec.gen.cfg @@ -0,0 +1,6 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.json +package: v1alpha1 +generate: + embedded-spec: true +output-options: + skip-prune: true diff --git a/api/gitops/v1alpha1/spec.gen.go b/api/gitops/v1alpha1/spec.gen.go new file mode 100644 index 0000000..0bba1fb --- /dev/null +++ b/api/gitops/v1alpha1/spec.gen.go @@ -0,0 +1,174 @@ +// Package v1alpha1 provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.7.0 DO NOT EDIT. +package v1alpha1 + +import ( + "bytes" + "compress/flate" + "encoding/base64" + "fmt" + "net/url" + "path" + "strings" + + "github.com/getkin/kin-openapi/openapi3" +) + +// Base64 encoded, compressed with deflate, json marshaled OpenAPI spec. +// Stored as a slice of fixed-width chunks rather than one concatenated +// const string: with thousands of chunks the chained `+` fold is several +// times slower for the Go compiler than parsing a slice literal. +var swaggerSpec = []string{ + "7Fp7c+M2kv8qKCZVM8mSEvW0raurK42t8ejKln2WnNpkPKeFyCaFCQkwAGhbmdJ3vwJAUuLDljPrJHeX", + "/c8W8Wh0//rXD+CL5bE4YRSoFNboi5VgjmOQwPV/50TeQMIEkYxvpv41lmv1sw/C4ySRhFFrZN1S8ksK", + "iPhAJQkIcBQwjuQaUEgk4sV8y7bgEcdJBNbISjjzU0+t4OAkEZZtEbVWonawLYpjNSgsb2/ZFodfUsLB", + "t0aSp2BbwltDjI3gUgJXa/z3R+z86jonn95mfzifvrj2sLPNf//uP761bEtuErWHkJzQ0Nput2p1kTAq", + "QJ99HHHA/mbySIRRjceoBCrVnzhJIuJhJX77s1Ba+LI7m9KPxCSyRmX9oQci14j46E3l8G8QNnshMJsp", + "ZQiJqacEdL3hUbgerp0jOBk6RwMPHOitjx3ohMPj3jronxwrlQmJZSqsUd89sS1JpNbyDQiWcg/qG2Sn", + "H1/cTMZnPy4nf5/OF3Nru6/RbzkE1sj6pr1DSNt8Fe0J54wbpZXBkGkNZWrb2tY77N/ALykI+ZVKfE8g", + "8tEbkYDXSnn0BsWpkIgyiVaAIE7kpqywo5Ne3w964PRXw57T756snJUbDJzVsd8buOB1hgMoKczdKWxK", + "73FEfMSNxGjPHQqdTWc/jC+mZ8vxzfnt5WS2eAWtvcM+ypW0ta33jK+I7wP9So3dCuDIZyC0ltb4HlAC", + "PCZCEEaRZAh7HgiB5JoIxDOMlJV4jPsDCPqBM/CO+s6ghz3H6wRDxzuB/rAT+N2jYVBSYm+nxLFZPShO", + "UajuenJzOZ3Pp1ez5dlkNp2cvYLudsra2taUKhLA0Rz4PXAz5+t0OKYopfCYgCfBR6BWQszzUs7BRw9r", + "EgFKOFMHJTTUbJdhpqzHLhyfkM/Hn52TsHPsnBxB6ISDz64T9sixO/i8Hnbcz3t6HJTBaA6DhD6NEWIf", + "h4vJzWx88Qo6LHYyekPZQNuaMfmepdR/Ff6r816BUc1LZc2drAbDIByEztA/HjjD/sp3/G545PhuMDjq", + "htA7PgpLCOw38J5aO9AHKNQ2u1os31/dzl4DezMmkdHP1rZuKU7lmnHyK3ytvn7Q5KOWUbHUTEAeBx1a", + "cSQQ5gZnKga+zJGHXrfnQ9d3enjQdfrdY+zgoTtw8JHf7bv+yh30/ZIaO3uOXBakCL6FLm9n49vFh8ls", + "MT0dL17Fm0tK3Bbr6QBcuHN5iv4Z5bEbBSyK2INyypv3p+jo2D1C15ytIojRmdazZib0YbG4RuPrqbij", + "mPpoPLl2Oic9g3x0kycCSEUdEmQaaN0pLks4S4BLYtKE3HZVqT6kMaaOCoZ4FQGCxyTC1OgxX1MZUFsu", + "4xXqAWKB5hLt6ma7Xc70okBYSWv2sfDSxI3sjl0mPupBVaYXBtuA8RhLa2SlnDgcAtCLNYmbA7GmT2Uv", + "8xF5zAf0NsbSW4PIuDezvhnx3b6MOsAXAhAqe93dxoRKCEFzXYb66sbzNePSRuuyPUUax5hvSvZCas2q", + "gl6SUexOb36oinCJvTWhsIcmvZ1SQ7YdTWNr9LGemtg1F7X3CNCu5n92Q4S2rffj6cXkbHl9Mzm9mp1N", + "F9OrmWXvwo/e44fx9GL87mJifSodvi7PS5Gw3U/2P5qvBTpyY30qprHVZ/B08lSKOboyKPkrTsjyHrgg", + "hoXLiv7BfMitWg5fhonQW2iFLRvdd3CUrHHnu9YdncZxKrVhcCCBK8be44udOvI5ikR25cr9R1WU/E1V", + "J5/+Zv7+tgkbelVYShI3QGRBYhASxwl6WAPNfcIEwQcsjEjgo7eKFHu93knJRayu2x067tDpuIuOO+q5", + "I9f9ad9YPpbg6J2VWbB/RaNNXoPVBPWJSCK8WZoqrsY6ArgTcALUjzYoG4vU2CfqRuQxGpAw5Y06vS5y", + "CjQ2hWSMHy+AhqpUHfZsKyY0/7fTIGzSWNMWCYT6jIhRqNHGSInnFOIREO0vlUJ1W5GxNqFe/e7hoTb6", + "ZfXsQbMoTj8Ul0uIn6sJJUp++VQzZWtbKfG/vmXQQgsVjQId+FSgTGWSSofRaIMw9e8oecrz0GINaHqG", + "PExVdGR6ZxxFmzy0gY/uCb6jv6TANztWRibh0ov8GyKBDq8JZ/fEB9/WImbZeAgUOJYgEEa3t9Oz1h29", + "o+918iFMOtHt7nxQicLovTovo6IK4joengXxb+51HMRGmvhfzy0RFhLFzDdafQHDdEf9wT/BMJXQsM/o", + "FfLJQF8LE7b16GBInCJl3jWQhFqy0cOXO1wuib9VqyZRynHU4OFqZ0LDNMK8+nUvSQuJZIlwYkxxCLzl", + "e3GLsHZl9LYa0y6IaaWU4xqFR7lMcAhLyX6Ghti2UD9rP+MgOYH7vG5VM5GaqcIeB5FGUgF0ohJKZHSO", + "CPVVDgxZqUFM1qUNr2ZW8Qyb/7z/Kf7p15/+/l/k6vPk4fKD++9NIS3brS7smHOsk6tKKMgNpgS8xJsi", + "8TWemkuPdGqoRdTubcQjEuLfxmK7jMzCSqAa9HL57Zr6D2Ym84yNK8nmfrmhjYUboiEF76lyROWz/B5H", + "SwEeo36m2gCnkbRGQ7dabl2zKDL2NdNUqMtmorcxoSROY9RxxXcVCw+bUupsvDXquE359S7SZuJYLasq", + "zxnh4BXNUpKTTcJaSBEC+nF8eYECEoFAvh6qAoHUoLyjOlZzxmRWJmMfvVX0LdKVn61LoHYUVZzvR+R2", + "M1A9Rj0SESwbE8eb0ne0gjW+JzpFr6UuZXspX9wsExYRryFZXWHvZxYEjcbsufUuoh6NViAfAGjm52LP", + "ppWj9563YpMRY/y4zNYtS2PXapZHDR6axisVVoNCHFUBp6uYSBRgEqW8yh6954RqQNa25mrbBufTPl81", + "27l2rawGUeX4gyKOJjutOKZeBb4xJrSG4Hd6YGWtHdiyOfWIVhM45dFTAhdccHtzUd1gLWUiRm0VRVrZ", + "ry2PxW0JOG77XqwTjFZIpHUgNa5QnRLnMKk9UbzPN9TLi/cmUmvSuAouS7Gh3sGsJKvWdDRSE5CK5HEi", + "91KRqpZ+SzZSI4NCMvCXHotjIhukM+UjMt/R/MMYyTWWu3xJpLp1F6Q6J9WL1Yhp5XW6PR+C/mDYJEcM", + "QuAQDvaesnEIr1gqtapMN0dmJmnd0cUmIZ5Ojz1GJSZUZD0GP2uZ6cxPW0PNAZUBTG5urm6qMo8jFUNM", + "x0lk5yqdtbHnU6xbP8tpLmoOIUUXDrqezM6ms/MRmlIiCY7MJ1slAXrkGotdu34DUk+a/zg7nZyN0Nnp", + "pRmP8h6SMhWW2jJzLfOptpueNZ0tr2+uzm8m8/kIXZAAvI0XAfLWmIYgUMBZbOZzQCtQ0VS3e7U9HaOl", + "EdIeoPhOZcg4lWsbUZAPjP9sq+JDZD2dIjplDZ3smJZtGdl136UQx7ItvXy56VKMPEgyW90eDFjercae", + "QnKtJ6uwrFR2TuRVItClSVfR+HqaV0a75rPx7zzsKWWcE+n4nNwDvaOnWOKIhVMJ8TQDCYoKjZo8OAYq", + "dS31zTcoL8OF+t9B33+/zzYExPffj9BpthfsZUZC8W+JK1XgyaBNaHhHEWqSRacXPgREgUrVaWiMBKji", + "UAL6h2JPk7L/I7/7QQmLIqGWy48MfnlTTNUPJjsAsYOesXNEPKBCwz678x4n2FsD6rZcK4sBBaU/PDy0", + "sP7cYjxsZ3NF+2J6OpnNJ0635bbWMo72mplWs90s2yp6YLuu1Na2WAIUJ8QaWb2W2+qbxsRa83G7VuOM", + "vlghyKZkSNcXujJOcEio7jxFRMiGjL6UISkqmqdJwrgUSk3Ux9zP11CJVapv23SxovPsLKNRcUMPmPrW", + "yFIFUgUo+iC7lw0f/9ni6GqlkAS+8f584F4BkPUsWKB0wOGesFQUXeqsHlEb6/pk9+Jhr4LYv0qpefKX", + "g9mWrnaUG3CQKacoAZ6XalPT1SgaITbKcho9vuO6T8un0j8toyC/QknEIi3qNLbbYyOg/uw+n2RuP1Ve", + "YnRd9wX3aS+7eKpX0g2XUPMiXhUmU87RN3I0LV/I29577qCndA5PKd95qUm9w5NK196Dl0jWdDeub9jM", + "VUbmOGX/VK6jSjcmGvz8VPeUlZdTeDjUtN1z7FQAd3ZtuOmZQPcEay96Q/w3qNqSU25JfIgTJoF6G4Ng", + "afp7RDzdoXsgUVS06RCj0EQW5hDlsv8AWVxlzcT6QZ5qY97Ry1TI7GoSnc3mTqfT7aEIryDK2srobcQe", + "gHtYANJ8TNMYOPHMq6H1JlkD1dnBNEAsJlI+3Y6kSHcjX/LequLexLeeeVP1Gx9UfTIFBAj5jvmb38eD", + "83db+6/CtjX66Pyemz9boOXXLqUMeEclryLTM686ylePK+ZvEONI/2riKeQPPf4gouq7J4dnlF/evR69", + "GVevFZ96VPvwpY4hwAiaqpQz/bto7NZVG0Bl/jEzD/BP08l3Q9pNzzQbomj/YD/BHK8Jrn8QOvqHZxRP", + "kl4PGMYGDcCwDye4KiGNnr+yRKsNIlIUrFwGwDnIP8T67p9GgkFurv/nMDoH2YihJG3EUBJh76so41Zf", + "1v1eoPlfEbH/PLCam9B/Rew/25kMyL86WLd3jxYOUHipH/qiPvXz/D3Pnyj9X2Lx/L1GQy28p5KsNwJ/", + "FTKvEPMeOl4Kw5Gao0upxgp6wUkYAtc1G4lj8InCfPmysRGI9h1dbRK8e3yeVC5ym5CqTPmHpBrdPwWk", + "0ijzL4HODDh7oNFA06oxDQFjT9NAbuOEtHdt3k/b/wkAAP//", +} + +// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, +// after base64-decoding and flate-decompressing the embedded blob. +func decodeSpec() ([]byte, error) { + encoded := strings.Join(swaggerSpec, "") + compressed, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return nil, fmt.Errorf("error base64 decoding spec: %w", err) + } + zr := flate.NewReader(bytes.NewReader(compressed)) + var buf bytes.Buffer + if _, err := buf.ReadFrom(zr); err != nil { + return nil, fmt.Errorf("read flate: %w", err) + } + if err := zr.Close(); err != nil { + return nil, fmt.Errorf("close flate reader: %w", err) + } + + return buf.Bytes(), nil +} + +var rawSpec = decodeSpecCached() + +// a naive cache of the decoded OpenAPI spec +func decodeSpecCached() func() ([]byte, error) { + data, err := decodeSpec() + return func() ([]byte, error) { + return data, err + } +} + +// Constructs a synthetic filesystem for resolving external references when loading openapi specifications. +func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { + res := make(map[string]func() ([]byte, error)) + if len(pathToFile) > 0 { + res[pathToFile] = rawSpec + } + + return res +} + +// GetSpec returns the OpenAPI specification corresponding to the generated +// code in this file. External references in the spec are resolved through +// PathToRawSpec; externally-referenced files must be embedded in their +// corresponding Go packages (via the import-mapping feature). URL-based +// external refs are not supported. +func GetSpec() (swagger *openapi3.T, err error) { + resolvePath := PathToRawSpec("") + + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { + pathToFile := url.String() + pathToFile = path.Clean(pathToFile) + getSpec, ok := resolvePath[pathToFile] + if !ok { + err1 := fmt.Errorf("path not found: %s", pathToFile) + return nil, err1 + } + return getSpec() + } + var specData []byte + specData, err = rawSpec() + if err != nil { + return + } + swagger, err = loader.LoadFromData(specData) + if err != nil { + return + } + return +} + +// GetSpecJSON returns the raw JSON bytes of the embedded OpenAPI +// specification: decompressed but not unmarshaled. External references +// are not resolved here; the bytes are the spec exactly as embedded by +// codegen. The result is cached at package init time, so repeated calls +// are cheap. +func GetSpecJSON() ([]byte, error) { + return rawSpec() +} + +// GetSwagger returns the OpenAPI specification corresponding to the +// generated code in this file. +// +// Deprecated: GetSwagger predates kin-openapi renaming openapi3.Swagger +// to openapi3.T. Use [GetSpec] instead. This wrapper is retained for +// backwards compatibility. +func GetSwagger() (*openapi3.T, error) { + return GetSpec() +} diff --git a/api/gitops/v1alpha1/types.gen.cfg b/api/gitops/v1alpha1/types.gen.cfg new file mode 100644 index 0000000..e34582e --- /dev/null +++ b/api/gitops/v1alpha1/types.gen.cfg @@ -0,0 +1,6 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.json +package: v1alpha1 +generate: + models: true +output-options: + skip-prune: true diff --git a/api/gitops/v1alpha1/types.gen.go b/api/gitops/v1alpha1/types.gen.go new file mode 100644 index 0000000..2581745 --- /dev/null +++ b/api/gitops/v1alpha1/types.gen.go @@ -0,0 +1,242 @@ +// Package v1alpha1 provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.7.0 DO NOT EDIT. +package v1alpha1 + +import ( + "time" +) + +// Defines values for ErrorType. +const ( + ALREADYEXISTS ErrorType = "ALREADY_EXISTS" + FAILEDPRECONDITION ErrorType = "FAILED_PRECONDITION" + INTERNAL ErrorType = "INTERNAL" + INVALIDARGUMENT ErrorType = "INVALID_ARGUMENT" + NOTFOUND ErrorType = "NOT_FOUND" + PERMISSIONDENIED ErrorType = "PERMISSION_DENIED" + UNAUTHENTICATED ErrorType = "UNAUTHENTICATED" + UNAVAILABLE ErrorType = "UNAVAILABLE" +) + +// Valid indicates whether the value is a known member of the ErrorType enum. +func (e ErrorType) Valid() bool { + switch e { + case ALREADYEXISTS: + return true + case FAILEDPRECONDITION: + return true + case INTERNAL: + return true + case INVALIDARGUMENT: + return true + case NOTFOUND: + return true + case PERMISSIONDENIED: + return true + case UNAUTHENTICATED: + return true + case UNAVAILABLE: + return true + default: + return false + } +} + +// Defines values for GitRepositoryStatusSyncState. +const ( + ERROR GitRepositoryStatusSyncState = "ERROR" + INPROGRESS GitRepositoryStatusSyncState = "IN_PROGRESS" + PENDING GitRepositoryStatusSyncState = "PENDING" + SYNCED GitRepositoryStatusSyncState = "SYNCED" +) + +// Valid indicates whether the value is a known member of the GitRepositoryStatusSyncState enum. +func (e GitRepositoryStatusSyncState) Valid() bool { + switch e { + case ERROR: + return true + case INPROGRESS: + return true + case PENDING: + return true + case SYNCED: + return true + default: + return false + } +} + +// Error Error response following RFC 7807 Problem Details for HTTP APIs +// and AEP-193 Error Responses specification. +type Error struct { + // Detail Human-readable explanation specific to this occurrence of the error. + Detail *string `json:"detail,omitempty"` + + // Instance Unique identifier for this specific error occurrence. + Instance *string `json:"instance,omitempty"` + + // Status HTTP status code (matches the response status) + Status int32 `json:"status"` + + // Title Short, human-readable summary of the error type. + Title string `json:"title"` + + // Type Machine-readable error code. + Type ErrorType `json:"type"` +} + +// ErrorType Machine-readable error code. +type ErrorType string + +// GitRepository defines model for GitRepository. +type GitRepository struct { + // ApiVersion Version of the GitRepository schema (e.g., v1alpha1). + // Immutable after creation. + ApiVersion string `json:"api_version"` + + // CreateTime Timestamp when the resource was created (RFC 3339) + CreateTime *time.Time `json:"create_time,omitempty"` + + // DisplayName User-friendly display name for the git repository configuration. + DisplayName string `json:"display_name"` + + // Path Resource path in the format: git-repositories/{gitRepositoryId} + Path *string `json:"path,omitempty"` + + // Spec Specification for a git repository connection. + Spec GitRepositorySpec `json:"spec"` + + // Status Sync status for a git repository. + Status *GitRepositoryStatus `json:"status,omitempty"` + + // Uid Unique identifier for the git repository. This field is output-only and + // immutable after creation. The ID can be optionally specified via + // query parameter on creation; if not provided, the server generates a UUID. + // + // Follows AEP-122 resource ID conventions. + Uid *string `json:"uid,omitempty"` + + // UpdateTime Timestamp when the resource was last modified (RFC 3339) + UpdateTime *time.Time `json:"update_time,omitempty"` +} + +// GitRepositoryList defines model for GitRepositoryList. +type GitRepositoryList struct { + // NextPageToken Token for retrieving the next page of results. + // Empty string indicates this is the last page. + NextPageToken string `json:"next_page_token"` + + // Results Array of git repository resources. + // May be empty if no results match the query. + Results []GitRepository `json:"results"` +} + +// GitRepositorySpec Specification for a git repository connection. +type GitRepositorySpec struct { + // IntervalSeconds Polling interval in seconds (minimum 10s). + IntervalSeconds *int32 `json:"interval_seconds,omitempty"` + + // Path Directory within the repo. Only YAML files directly at this + // path root are read (not subdirectories). + Path *string `json:"path,omitempty"` + + // Reconciliation Reconciliation behavior configuration. + Reconciliation *struct { + RetryPolicy *struct { + // BackoffSeconds Backoff between retries in seconds. + BackoffSeconds *int32 `json:"backoff_seconds,omitempty"` + + // MaxRetries Maximum number of retries on submit failure. + MaxRetries *int32 `json:"max_retries,omitempty"` + } `json:"retry_policy,omitempty"` + } `json:"reconciliation,omitempty"` + + // Ref Git reference to watch. + Ref *struct { + // Branch Branch to watch. + Branch *string `json:"branch,omitempty"` + } `json:"ref,omitempty"` + + // Url Git repository URL. + Url string `json:"url"` +} + +// GitRepositoryStatus Sync status for a git repository. +type GitRepositoryStatus struct { + // LastSyncTime Timestamp of the last sync attempt (RFC 3339). + LastSyncTime *time.Time `json:"last_sync_time,omitempty"` + + // LastSyncedCommit The Git commit SHA that was last successfully synced. + LastSyncedCommit *string `json:"last_synced_commit,omitempty"` + + // Message Human-readable message about the current status. + // Typically contains error details when sync_state is ERROR. + Message *string `json:"message,omitempty"` + + // SyncState Current sync state. + // - PENDING: Initial state, no sync has occurred yet. + // - SYNCED: DCM state matches Git at lastSyncedCommit. + // - IN_PROGRESS: Lifecycle changes from Git are being applied. + // - ERROR: Sync failed (auth, network, parse errors). + SyncState *GitRepositoryStatusSyncState `json:"sync_state,omitempty"` +} + +// GitRepositoryStatusSyncState Current sync state. +// - PENDING: Initial state, no sync has occurred yet. +// - SYNCED: DCM state matches Git at lastSyncedCommit. +// - IN_PROGRESS: Lifecycle changes from Git are being applied. +// - ERROR: Sync failed (auth, network, parse errors). +type GitRepositoryStatusSyncState string + +// GitRepositoryIdPath defines model for GitRepositoryIdPath. +type GitRepositoryIdPath = string + +// AlreadyExists Error response following RFC 7807 Problem Details for HTTP APIs +// and AEP-193 Error Responses specification. +type AlreadyExists = Error + +// BadRequest Error response following RFC 7807 Problem Details for HTTP APIs +// and AEP-193 Error Responses specification. +type BadRequest = Error + +// Forbidden Error response following RFC 7807 Problem Details for HTTP APIs +// and AEP-193 Error Responses specification. +type Forbidden = Error + +// InternalServerError Error response following RFC 7807 Problem Details for HTTP APIs +// and AEP-193 Error Responses specification. +type InternalServerError = Error + +// NotFound Error response following RFC 7807 Problem Details for HTTP APIs +// and AEP-193 Error Responses specification. +type NotFound = Error + +// Unauthorized Error response following RFC 7807 Problem Details for HTTP APIs +// and AEP-193 Error Responses specification. +type Unauthorized = Error + +// ListGitRepositoriesParams defines parameters for ListGitRepositories. +type ListGitRepositoriesParams struct { + // PageToken Token for retrieving the next page of results. + // Obtained from the next_page_token field of a previous response. + PageToken *string `form:"page_token,omitempty" json:"page_token,omitempty"` + + // MaxPageSize Maximum number of items to return per page. + // If not specified, defaults to 100. + MaxPageSize *int32 `form:"max_page_size,omitempty" json:"max_page_size,omitempty"` +} + +// CreateGitRepositoryParams defines parameters for CreateGitRepository. +type CreateGitRepositoryParams struct { + // Id Optional user-specified ID for the git repository. + // Must follow DNS-1123 label format (lowercase alphanumeric with hyphens). + // If omitted, the server generates an ID. + Id *string `form:"id,omitempty" json:"id,omitempty"` +} + +// CreateGitRepositoryJSONRequestBody defines body for CreateGitRepository for application/json ContentType. +type CreateGitRepositoryJSONRequestBody = GitRepository + +// UpdateGitRepositoryJSONRequestBody defines body for UpdateGitRepository for application/json ContentType. +type UpdateGitRepositoryJSONRequestBody = GitRepository diff --git a/internal/gitops/api/server/server.gen.cfg b/internal/gitops/api/server/server.gen.cfg new file mode 100644 index 0000000..665ce30 --- /dev/null +++ b/internal/gitops/api/server/server.gen.cfg @@ -0,0 +1,10 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.json +package: server +generate: + chi-server: true + strict-server: true +additional-imports: + - alias: . + package: github.com/dcm-project/control-plane/api/gitops/v1alpha1 +output-options: + skip-prune: true diff --git a/internal/gitops/api/server/server.gen.go b/internal/gitops/api/server/server.gen.go new file mode 100644 index 0000000..98868d0 --- /dev/null +++ b/internal/gitops/api/server/server.gen.go @@ -0,0 +1,1290 @@ +// Package server provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.7.0 DO NOT EDIT. +package server + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + + . "github.com/dcm-project/control-plane/api/gitops/v1alpha1" + "github.com/go-chi/chi/v5" + "github.com/oapi-codegen/runtime" +) + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // List git repositories + // (GET /git-repositories) + ListGitRepositories(w http.ResponseWriter, r *http.Request, params ListGitRepositoriesParams) + // Create a git repository + // (POST /git-repositories) + CreateGitRepository(w http.ResponseWriter, r *http.Request, params CreateGitRepositoryParams) + // Delete a git repository + // (DELETE /git-repositories/{gitRepositoryId}) + DeleteGitRepository(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) + // Get a git repository + // (GET /git-repositories/{gitRepositoryId}) + GetGitRepository(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) + // Update a git repository + // (PUT /git-repositories/{gitRepositoryId}) + UpdateGitRepository(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) + // Get git repository sync status + // (GET /git-repositories/{gitRepositoryId}/status) + GetGitRepositoryStatus(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) + // Trigger immediate sync + // (POST /git-repositories/{gitRepositoryId}:sync) + SyncGitRepository(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) +} + +// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. + +type Unimplemented struct{} + +// List git repositories +// (GET /git-repositories) +func (_ Unimplemented) ListGitRepositories(w http.ResponseWriter, r *http.Request, params ListGitRepositoriesParams) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Create a git repository +// (POST /git-repositories) +func (_ Unimplemented) CreateGitRepository(w http.ResponseWriter, r *http.Request, params CreateGitRepositoryParams) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Delete a git repository +// (DELETE /git-repositories/{gitRepositoryId}) +func (_ Unimplemented) DeleteGitRepository(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Get a git repository +// (GET /git-repositories/{gitRepositoryId}) +func (_ Unimplemented) GetGitRepository(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Update a git repository +// (PUT /git-repositories/{gitRepositoryId}) +func (_ Unimplemented) UpdateGitRepository(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Get git repository sync status +// (GET /git-repositories/{gitRepositoryId}/status) +func (_ Unimplemented) GetGitRepositoryStatus(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Trigger immediate sync +// (POST /git-repositories/{gitRepositoryId}:sync) +func (_ Unimplemented) SyncGitRepository(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) { + w.WriteHeader(http.StatusNotImplemented) +} + +// ServerInterfaceWrapper converts contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +type MiddlewareFunc func(http.Handler) http.Handler + +// ListGitRepositories operation middleware +func (siw *ServerInterfaceWrapper) ListGitRepositories(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // Parameter object where we will unmarshal all parameters from the context + var params ListGitRepositoriesParams + + // ------------- Optional query parameter "page_token" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "page_token", r.URL.Query(), ¶ms.PageToken, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "page_token"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "page_token", Err: err}) + } + return + } + + // ------------- Optional query parameter "max_page_size" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "max_page_size", r.URL.Query(), ¶ms.MaxPageSize, runtime.BindQueryParameterOptions{Type: "integer", Format: "int32"}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "max_page_size"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "max_page_size", Err: err}) + } + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ListGitRepositories(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// CreateGitRepository operation middleware +func (siw *ServerInterfaceWrapper) CreateGitRepository(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // Parameter object where we will unmarshal all parameters from the context + var params CreateGitRepositoryParams + + // ------------- Optional query parameter "id" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "id", r.URL.Query(), ¶ms.Id, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "id"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + } + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.CreateGitRepository(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// DeleteGitRepository operation middleware +func (siw *ServerInterfaceWrapper) DeleteGitRepository(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "gitRepositoryId" ------------- + var gitRepositoryId GitRepositoryIdPath + + err = runtime.BindStyledParameterWithOptions("simple", "gitRepositoryId", chi.URLParam(r, "gitRepositoryId"), &gitRepositoryId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "gitRepositoryId", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DeleteGitRepository(w, r, gitRepositoryId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetGitRepository operation middleware +func (siw *ServerInterfaceWrapper) GetGitRepository(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "gitRepositoryId" ------------- + var gitRepositoryId GitRepositoryIdPath + + err = runtime.BindStyledParameterWithOptions("simple", "gitRepositoryId", chi.URLParam(r, "gitRepositoryId"), &gitRepositoryId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "gitRepositoryId", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetGitRepository(w, r, gitRepositoryId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// UpdateGitRepository operation middleware +func (siw *ServerInterfaceWrapper) UpdateGitRepository(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "gitRepositoryId" ------------- + var gitRepositoryId GitRepositoryIdPath + + err = runtime.BindStyledParameterWithOptions("simple", "gitRepositoryId", chi.URLParam(r, "gitRepositoryId"), &gitRepositoryId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "gitRepositoryId", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.UpdateGitRepository(w, r, gitRepositoryId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetGitRepositoryStatus operation middleware +func (siw *ServerInterfaceWrapper) GetGitRepositoryStatus(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "gitRepositoryId" ------------- + var gitRepositoryId GitRepositoryIdPath + + err = runtime.BindStyledParameterWithOptions("simple", "gitRepositoryId", chi.URLParam(r, "gitRepositoryId"), &gitRepositoryId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "gitRepositoryId", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetGitRepositoryStatus(w, r, gitRepositoryId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// SyncGitRepository operation middleware +func (siw *ServerInterfaceWrapper) SyncGitRepository(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "gitRepositoryId" ------------- + var gitRepositoryId GitRepositoryIdPath + + err = runtime.BindStyledParameterWithOptions("simple", "gitRepositoryId", chi.URLParam(r, "gitRepositoryId"), &gitRepositoryId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "gitRepositoryId", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.SyncGitRepository(w, r, gitRepositoryId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +type UnescapedCookieParamError struct { + ParamName string + Err error +} + +func (e *UnescapedCookieParamError) Error() string { + return fmt.Sprintf("error unescaping cookie parameter '%s'", e.ParamName) +} + +func (e *UnescapedCookieParamError) Unwrap() error { + return e.Err +} + +type UnmarshalingParamError struct { + ParamName string + Err error +} + +func (e *UnmarshalingParamError) Error() string { + return fmt.Sprintf("Error unmarshaling parameter %s as JSON: %s", e.ParamName, e.Err.Error()) +} + +func (e *UnmarshalingParamError) Unwrap() error { + return e.Err +} + +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +type RequiredHeaderError struct { + ParamName string + Err error +} + +func (e *RequiredHeaderError) Error() string { + return fmt.Sprintf("Header parameter %s is required, but not found", e.ParamName) +} + +func (e *RequiredHeaderError) Unwrap() error { + return e.Err +} + +type InvalidParamFormatError struct { + ParamName string + Err error +} + +func (e *InvalidParamFormatError) Error() string { + return fmt.Sprintf("Invalid format for parameter %s: %s", e.ParamName, e.Err.Error()) +} + +func (e *InvalidParamFormatError) Unwrap() error { + return e.Err +} + +type TooManyValuesForParamError struct { + ParamName string + Count int +} + +func (e *TooManyValuesForParamError) Error() string { + return fmt.Sprintf("Expected one value for %s, got %d", e.ParamName, e.Count) +} + +// Handler creates http.Handler with routing matching OpenAPI spec. +func Handler(si ServerInterface) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{}) +} + +type ChiServerOptions struct { + BaseURL string + BaseRouter chi.Router + Middlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux. +func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{ + BaseRouter: r, + }) +} + +func HandlerFromMuxWithBaseURL(si ServerInterface, r chi.Router, baseURL string) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{ + BaseURL: baseURL, + BaseRouter: r, + }) +} + +// HandlerWithOptions creates http.Handler with additional options +func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handler { + r := options.BaseRouter + + if r == nil { + r = chi.NewRouter() + } + if options.ErrorHandlerFunc == nil { + options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + } + } + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandlerFunc: options.ErrorHandlerFunc, + } + + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/git-repositories", wrapper.ListGitRepositories) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/git-repositories", wrapper.CreateGitRepository) + }) + r.Group(func(r chi.Router) { + r.Delete(options.BaseURL+"/git-repositories/{gitRepositoryId}", wrapper.DeleteGitRepository) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/git-repositories/{gitRepositoryId}", wrapper.GetGitRepository) + }) + r.Group(func(r chi.Router) { + r.Put(options.BaseURL+"/git-repositories/{gitRepositoryId}", wrapper.UpdateGitRepository) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/git-repositories/{gitRepositoryId}/status", wrapper.GetGitRepositoryStatus) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/git-repositories/{gitRepositoryId}:sync", wrapper.SyncGitRepository) + }) + + return r +} + +type AlreadyExistsJSONResponse Error + +type BadRequestJSONResponse Error + +type ForbiddenJSONResponse Error + +type InternalServerErrorJSONResponse Error + +type NotFoundJSONResponse Error + +type UnauthorizedJSONResponse Error + +type ListGitRepositoriesRequestObject struct { + Params ListGitRepositoriesParams +} + +type ListGitRepositoriesResponseObject interface { + VisitListGitRepositoriesResponse(w http.ResponseWriter) error +} + +type ListGitRepositories200JSONResponse GitRepositoryList + +func (response ListGitRepositories200JSONResponse) VisitListGitRepositoriesResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + _, err := buf.WriteTo(w) + return err +} + +type ListGitRepositories400JSONResponse struct{ BadRequestJSONResponse } + +func (response ListGitRepositories400JSONResponse) VisitListGitRepositoriesResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + _, err := buf.WriteTo(w) + return err +} + +type ListGitRepositories401JSONResponse struct{ UnauthorizedJSONResponse } + +func (response ListGitRepositories401JSONResponse) VisitListGitRepositoriesResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + _, err := buf.WriteTo(w) + return err +} + +type ListGitRepositories403JSONResponse struct{ ForbiddenJSONResponse } + +func (response ListGitRepositories403JSONResponse) VisitListGitRepositoriesResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + _, err := buf.WriteTo(w) + return err +} + +type ListGitRepositories500JSONResponse struct { + InternalServerErrorJSONResponse +} + +func (response ListGitRepositories500JSONResponse) VisitListGitRepositoriesResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + _, err := buf.WriteTo(w) + return err +} + +type CreateGitRepositoryRequestObject struct { + Params CreateGitRepositoryParams + Body *CreateGitRepositoryJSONRequestBody +} + +type CreateGitRepositoryResponseObject interface { + VisitCreateGitRepositoryResponse(w http.ResponseWriter) error +} + +type CreateGitRepository201JSONResponse GitRepository + +func (response CreateGitRepository201JSONResponse) VisitCreateGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(201) + _, err := buf.WriteTo(w) + return err +} + +type CreateGitRepository400JSONResponse Error + +func (response CreateGitRepository400JSONResponse) VisitCreateGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + _, err := buf.WriteTo(w) + return err +} + +type CreateGitRepository401JSONResponse struct{ UnauthorizedJSONResponse } + +func (response CreateGitRepository401JSONResponse) VisitCreateGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + _, err := buf.WriteTo(w) + return err +} + +type CreateGitRepository403JSONResponse struct{ ForbiddenJSONResponse } + +func (response CreateGitRepository403JSONResponse) VisitCreateGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + _, err := buf.WriteTo(w) + return err +} + +type CreateGitRepository409JSONResponse struct{ AlreadyExistsJSONResponse } + +func (response CreateGitRepository409JSONResponse) VisitCreateGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + _, err := buf.WriteTo(w) + return err +} + +type CreateGitRepository500JSONResponse struct { + InternalServerErrorJSONResponse +} + +func (response CreateGitRepository500JSONResponse) VisitCreateGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + _, err := buf.WriteTo(w) + return err +} + +type DeleteGitRepositoryRequestObject struct { + GitRepositoryId GitRepositoryIdPath `json:"gitRepositoryId"` +} + +type DeleteGitRepositoryResponseObject interface { + VisitDeleteGitRepositoryResponse(w http.ResponseWriter) error +} + +type DeleteGitRepository204Response struct { +} + +func (response DeleteGitRepository204Response) VisitDeleteGitRepositoryResponse(w http.ResponseWriter) error { + w.WriteHeader(204) + return nil +} + +type DeleteGitRepository401JSONResponse struct{ UnauthorizedJSONResponse } + +func (response DeleteGitRepository401JSONResponse) VisitDeleteGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + _, err := buf.WriteTo(w) + return err +} + +type DeleteGitRepository403JSONResponse struct{ ForbiddenJSONResponse } + +func (response DeleteGitRepository403JSONResponse) VisitDeleteGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + _, err := buf.WriteTo(w) + return err +} + +type DeleteGitRepository404JSONResponse struct{ NotFoundJSONResponse } + +func (response DeleteGitRepository404JSONResponse) VisitDeleteGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + _, err := buf.WriteTo(w) + return err +} + +type DeleteGitRepository500JSONResponse struct { + InternalServerErrorJSONResponse +} + +func (response DeleteGitRepository500JSONResponse) VisitDeleteGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + _, err := buf.WriteTo(w) + return err +} + +type GetGitRepositoryRequestObject struct { + GitRepositoryId GitRepositoryIdPath `json:"gitRepositoryId"` +} + +type GetGitRepositoryResponseObject interface { + VisitGetGitRepositoryResponse(w http.ResponseWriter) error +} + +type GetGitRepository200JSONResponse GitRepository + +func (response GetGitRepository200JSONResponse) VisitGetGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + _, err := buf.WriteTo(w) + return err +} + +type GetGitRepository401JSONResponse struct{ UnauthorizedJSONResponse } + +func (response GetGitRepository401JSONResponse) VisitGetGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + _, err := buf.WriteTo(w) + return err +} + +type GetGitRepository403JSONResponse struct{ ForbiddenJSONResponse } + +func (response GetGitRepository403JSONResponse) VisitGetGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + _, err := buf.WriteTo(w) + return err +} + +type GetGitRepository404JSONResponse struct{ NotFoundJSONResponse } + +func (response GetGitRepository404JSONResponse) VisitGetGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + _, err := buf.WriteTo(w) + return err +} + +type GetGitRepository500JSONResponse struct { + InternalServerErrorJSONResponse +} + +func (response GetGitRepository500JSONResponse) VisitGetGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + _, err := buf.WriteTo(w) + return err +} + +type UpdateGitRepositoryRequestObject struct { + GitRepositoryId GitRepositoryIdPath `json:"gitRepositoryId"` + Body *UpdateGitRepositoryJSONRequestBody +} + +type UpdateGitRepositoryResponseObject interface { + VisitUpdateGitRepositoryResponse(w http.ResponseWriter) error +} + +type UpdateGitRepository200JSONResponse GitRepository + +func (response UpdateGitRepository200JSONResponse) VisitUpdateGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + _, err := buf.WriteTo(w) + return err +} + +type UpdateGitRepository400JSONResponse Error + +func (response UpdateGitRepository400JSONResponse) VisitUpdateGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + _, err := buf.WriteTo(w) + return err +} + +type UpdateGitRepository401JSONResponse struct{ UnauthorizedJSONResponse } + +func (response UpdateGitRepository401JSONResponse) VisitUpdateGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + _, err := buf.WriteTo(w) + return err +} + +type UpdateGitRepository403JSONResponse struct{ ForbiddenJSONResponse } + +func (response UpdateGitRepository403JSONResponse) VisitUpdateGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + _, err := buf.WriteTo(w) + return err +} + +type UpdateGitRepository404JSONResponse struct{ NotFoundJSONResponse } + +func (response UpdateGitRepository404JSONResponse) VisitUpdateGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + _, err := buf.WriteTo(w) + return err +} + +type UpdateGitRepository500JSONResponse struct { + InternalServerErrorJSONResponse +} + +func (response UpdateGitRepository500JSONResponse) VisitUpdateGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + _, err := buf.WriteTo(w) + return err +} + +type GetGitRepositoryStatusRequestObject struct { + GitRepositoryId GitRepositoryIdPath `json:"gitRepositoryId"` +} + +type GetGitRepositoryStatusResponseObject interface { + VisitGetGitRepositoryStatusResponse(w http.ResponseWriter) error +} + +type GetGitRepositoryStatus200JSONResponse GitRepositoryStatus + +func (response GetGitRepositoryStatus200JSONResponse) VisitGetGitRepositoryStatusResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + _, err := buf.WriteTo(w) + return err +} + +type GetGitRepositoryStatus401JSONResponse struct{ UnauthorizedJSONResponse } + +func (response GetGitRepositoryStatus401JSONResponse) VisitGetGitRepositoryStatusResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + _, err := buf.WriteTo(w) + return err +} + +type GetGitRepositoryStatus403JSONResponse struct{ ForbiddenJSONResponse } + +func (response GetGitRepositoryStatus403JSONResponse) VisitGetGitRepositoryStatusResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + _, err := buf.WriteTo(w) + return err +} + +type GetGitRepositoryStatus404JSONResponse struct{ NotFoundJSONResponse } + +func (response GetGitRepositoryStatus404JSONResponse) VisitGetGitRepositoryStatusResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + _, err := buf.WriteTo(w) + return err +} + +type GetGitRepositoryStatus500JSONResponse struct { + InternalServerErrorJSONResponse +} + +func (response GetGitRepositoryStatus500JSONResponse) VisitGetGitRepositoryStatusResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + _, err := buf.WriteTo(w) + return err +} + +type SyncGitRepositoryRequestObject struct { + GitRepositoryId GitRepositoryIdPath `json:"gitRepositoryId"` +} + +type SyncGitRepositoryResponseObject interface { + VisitSyncGitRepositoryResponse(w http.ResponseWriter) error +} + +type SyncGitRepository202JSONResponse GitRepositoryStatus + +func (response SyncGitRepository202JSONResponse) VisitSyncGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(202) + _, err := buf.WriteTo(w) + return err +} + +type SyncGitRepository401JSONResponse struct{ UnauthorizedJSONResponse } + +func (response SyncGitRepository401JSONResponse) VisitSyncGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + _, err := buf.WriteTo(w) + return err +} + +type SyncGitRepository403JSONResponse struct{ ForbiddenJSONResponse } + +func (response SyncGitRepository403JSONResponse) VisitSyncGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + _, err := buf.WriteTo(w) + return err +} + +type SyncGitRepository404JSONResponse struct{ NotFoundJSONResponse } + +func (response SyncGitRepository404JSONResponse) VisitSyncGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + _, err := buf.WriteTo(w) + return err +} + +type SyncGitRepository500JSONResponse struct { + InternalServerErrorJSONResponse +} + +func (response SyncGitRepository500JSONResponse) VisitSyncGitRepositoryResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + _, err := buf.WriteTo(w) + return err +} + +// StrictServerInterface represents all server handlers. +type StrictServerInterface interface { + // List git repositories + // (GET /git-repositories) + ListGitRepositories(ctx context.Context, request ListGitRepositoriesRequestObject) (ListGitRepositoriesResponseObject, error) + // Create a git repository + // (POST /git-repositories) + CreateGitRepository(ctx context.Context, request CreateGitRepositoryRequestObject) (CreateGitRepositoryResponseObject, error) + // Delete a git repository + // (DELETE /git-repositories/{gitRepositoryId}) + DeleteGitRepository(ctx context.Context, request DeleteGitRepositoryRequestObject) (DeleteGitRepositoryResponseObject, error) + // Get a git repository + // (GET /git-repositories/{gitRepositoryId}) + GetGitRepository(ctx context.Context, request GetGitRepositoryRequestObject) (GetGitRepositoryResponseObject, error) + // Update a git repository + // (PUT /git-repositories/{gitRepositoryId}) + UpdateGitRepository(ctx context.Context, request UpdateGitRepositoryRequestObject) (UpdateGitRepositoryResponseObject, error) + // Get git repository sync status + // (GET /git-repositories/{gitRepositoryId}/status) + GetGitRepositoryStatus(ctx context.Context, request GetGitRepositoryStatusRequestObject) (GetGitRepositoryStatusResponseObject, error) + // Trigger immediate sync + // (POST /git-repositories/{gitRepositoryId}:sync) + SyncGitRepository(ctx context.Context, request SyncGitRepositoryRequestObject) (SyncGitRepositoryResponseObject, error) +} + +type StrictHandlerFunc func(ctx context.Context, w http.ResponseWriter, r *http.Request, request any) (any, error) +type StrictMiddlewareFunc func(f StrictHandlerFunc, operationID string) StrictHandlerFunc + +type StrictHTTPServerOptions struct { + RequestErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) + ResponseErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares, options: StrictHTTPServerOptions{ + RequestErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + }, + ResponseErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusInternalServerError) + }, + }} +} + +func NewStrictHandlerWithOptions(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc, options StrictHTTPServerOptions) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares, options: options} +} + +type strictHandler struct { + ssi StrictServerInterface + middlewares []StrictMiddlewareFunc + options StrictHTTPServerOptions +} + +// ListGitRepositories operation middleware +func (sh *strictHandler) ListGitRepositories(w http.ResponseWriter, r *http.Request, params ListGitRepositoriesParams) { + var request ListGitRepositoriesRequestObject + + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.ListGitRepositories(ctx, request.(ListGitRepositoriesRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ListGitRepositories") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ListGitRepositoriesResponseObject); ok { + if err := validResponse.VisitListGitRepositoriesResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// CreateGitRepository operation middleware +func (sh *strictHandler) CreateGitRepository(w http.ResponseWriter, r *http.Request, params CreateGitRepositoryParams) { + var request CreateGitRepositoryRequestObject + + request.Params = params + + var body CreateGitRepositoryJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.CreateGitRepository(ctx, request.(CreateGitRepositoryRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "CreateGitRepository") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(CreateGitRepositoryResponseObject); ok { + if err := validResponse.VisitCreateGitRepositoryResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// DeleteGitRepository operation middleware +func (sh *strictHandler) DeleteGitRepository(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) { + var request DeleteGitRepositoryRequestObject + + request.GitRepositoryId = gitRepositoryId + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.DeleteGitRepository(ctx, request.(DeleteGitRepositoryRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "DeleteGitRepository") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(DeleteGitRepositoryResponseObject); ok { + if err := validResponse.VisitDeleteGitRepositoryResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetGitRepository operation middleware +func (sh *strictHandler) GetGitRepository(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) { + var request GetGitRepositoryRequestObject + + request.GitRepositoryId = gitRepositoryId + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetGitRepository(ctx, request.(GetGitRepositoryRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetGitRepository") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetGitRepositoryResponseObject); ok { + if err := validResponse.VisitGetGitRepositoryResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// UpdateGitRepository operation middleware +func (sh *strictHandler) UpdateGitRepository(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) { + var request UpdateGitRepositoryRequestObject + + request.GitRepositoryId = gitRepositoryId + + var body UpdateGitRepositoryJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.UpdateGitRepository(ctx, request.(UpdateGitRepositoryRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "UpdateGitRepository") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(UpdateGitRepositoryResponseObject); ok { + if err := validResponse.VisitUpdateGitRepositoryResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetGitRepositoryStatus operation middleware +func (sh *strictHandler) GetGitRepositoryStatus(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) { + var request GetGitRepositoryStatusRequestObject + + request.GitRepositoryId = gitRepositoryId + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetGitRepositoryStatus(ctx, request.(GetGitRepositoryStatusRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetGitRepositoryStatus") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetGitRepositoryStatusResponseObject); ok { + if err := validResponse.VisitGetGitRepositoryStatusResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// SyncGitRepository operation middleware +func (sh *strictHandler) SyncGitRepository(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) { + var request SyncGitRepositoryRequestObject + + request.GitRepositoryId = gitRepositoryId + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.SyncGitRepository(ctx, request.(SyncGitRepositoryRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "SyncGitRepository") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(SyncGitRepositoryResponseObject); ok { + if err := validResponse.VisitSyncGitRepositoryResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} diff --git a/make/gitops.mk b/make/gitops.mk new file mode 100644 index 0000000..38be747 --- /dev/null +++ b/make/gitops.mk @@ -0,0 +1,48 @@ +# GitOps domain (codegen and subsystem tests). +GITOPS_DOMAIN := gitops +GITOPS_API := api/$(GITOPS_DOMAIN)/v1alpha1 +GITOPS_SERVER_DIR := internal/$(GITOPS_DOMAIN)/api/server +GITOPS_CLIENT_DIR := pkg/$(GITOPS_DOMAIN)/client + +generate-gitops-types: + go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen \ + --config=$(GITOPS_API)/types.gen.cfg \ + -o $(GITOPS_API)/types.gen.go \ + $(GITOPS_API)/openapi.yaml + +generate-gitops-spec: + go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen \ + --config=$(GITOPS_API)/spec.gen.cfg \ + -o $(GITOPS_API)/spec.gen.go \ + $(GITOPS_API)/openapi.yaml + +generate-gitops-server: + go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen \ + --config=$(GITOPS_SERVER_DIR)/server.gen.cfg \ + -o $(GITOPS_SERVER_DIR)/server.gen.go \ + $(GITOPS_API)/openapi.yaml + +generate-gitops-client: + go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen \ + --config=$(GITOPS_CLIENT_DIR)/client.gen.cfg \ + -o $(GITOPS_CLIENT_DIR)/client.gen.go \ + $(GITOPS_API)/openapi.yaml + +generate-gitops-api: generate-gitops-types generate-gitops-spec generate-gitops-server generate-gitops-client + +test-gitops: + $(GINKGO) $(GINKGO_FLAGS) ./internal/$(GITOPS_DOMAIN) ./pkg/$(GITOPS_DOMAIN) + +gitops-subsystem-test-up: + $(COMPOSE) -f test/subsystem/$(GITOPS_DOMAIN)/docker-compose.yaml up -d --build + +gitops-subsystem-test-down: + $(COMPOSE) -f test/subsystem/$(GITOPS_DOMAIN)/docker-compose.yaml down -v + +gitops-subsystem-test: + $(GINKGO) $(GINKGO_FLAGS) -tags=subsystem ./test/subsystem/$(GITOPS_DOMAIN) + +.PHONY: generate-gitops-types generate-gitops-spec \ + generate-gitops-server generate-gitops-client generate-gitops-api \ + test-gitops gitops-subsystem-test-up gitops-subsystem-test-down \ + gitops-subsystem-test diff --git a/pkg/gitops/client/client.gen.cfg b/pkg/gitops/client/client.gen.cfg new file mode 100644 index 0000000..75b0c9f --- /dev/null +++ b/pkg/gitops/client/client.gen.cfg @@ -0,0 +1,9 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.json +package: client +generate: + client: true +additional-imports: + - alias: . + package: github.com/dcm-project/control-plane/api/gitops/v1alpha1 +output-options: + skip-prune: true diff --git a/pkg/gitops/client/client.gen.go b/pkg/gitops/client/client.gen.go new file mode 100644 index 0000000..e24254b --- /dev/null +++ b/pkg/gitops/client/client.gen.go @@ -0,0 +1,1313 @@ +// Package client provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.7.0 DO NOT EDIT. +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + . "github.com/dcm-project/control-plane/api/gitops/v1alpha1" + "github.com/oapi-codegen/runtime" +) + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // Doer for performing requests, typically a *http.Client with any + // customized settings, such as certificate chains. + Client HttpRequestDoer + + // A list of callbacks for modifying requests which are generated before sending over + // the network. + RequestEditors []RequestEditorFn +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // ListGitRepositories request + ListGitRepositories(ctx context.Context, params *ListGitRepositoriesParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // CreateGitRepositoryWithBody request with any body + CreateGitRepositoryWithBody(ctx context.Context, params *CreateGitRepositoryParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + CreateGitRepository(ctx context.Context, params *CreateGitRepositoryParams, body CreateGitRepositoryJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DeleteGitRepository request + DeleteGitRepository(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetGitRepository request + GetGitRepository(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*http.Response, error) + + // UpdateGitRepositoryWithBody request with any body + UpdateGitRepositoryWithBody(ctx context.Context, gitRepositoryId GitRepositoryIdPath, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + UpdateGitRepository(ctx context.Context, gitRepositoryId GitRepositoryIdPath, body UpdateGitRepositoryJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetGitRepositoryStatus request + GetGitRepositoryStatus(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*http.Response, error) + + // SyncGitRepository request + SyncGitRepository(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) ListGitRepositories(ctx context.Context, params *ListGitRepositoriesParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewListGitRepositoriesRequest(c.Server, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) CreateGitRepositoryWithBody(ctx context.Context, params *CreateGitRepositoryParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateGitRepositoryRequestWithBody(c.Server, params, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) CreateGitRepository(ctx context.Context, params *CreateGitRepositoryParams, body CreateGitRepositoryJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateGitRepositoryRequest(c.Server, params, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) DeleteGitRepository(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteGitRepositoryRequest(c.Server, gitRepositoryId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetGitRepository(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetGitRepositoryRequest(c.Server, gitRepositoryId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) UpdateGitRepositoryWithBody(ctx context.Context, gitRepositoryId GitRepositoryIdPath, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUpdateGitRepositoryRequestWithBody(c.Server, gitRepositoryId, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) UpdateGitRepository(ctx context.Context, gitRepositoryId GitRepositoryIdPath, body UpdateGitRepositoryJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUpdateGitRepositoryRequest(c.Server, gitRepositoryId, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetGitRepositoryStatus(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetGitRepositoryStatusRequest(c.Server, gitRepositoryId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) SyncGitRepository(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewSyncGitRepositoryRequest(c.Server, gitRepositoryId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +// NewListGitRepositoriesRequest generates requests for ListGitRepositories +func NewListGitRepositoriesRequest(server string, params *ListGitRepositoriesParams) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/git-repositories") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). + queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string + + if params.PageToken != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "page_token", *params.PageToken, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + } + + if params.MaxPageSize != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "max_page_size", *params.MaxPageSize, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: "int32"}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + } + + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") + } + + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewCreateGitRepositoryRequest calls the generic CreateGitRepository builder with application/json body +func NewCreateGitRepositoryRequest(server string, params *CreateGitRepositoryParams, body CreateGitRepositoryJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewCreateGitRepositoryRequestWithBody(server, params, "application/json", bodyReader) +} + +// NewCreateGitRepositoryRequestWithBody generates requests for CreateGitRepository with any type of body +func NewCreateGitRepositoryRequestWithBody(server string, params *CreateGitRepositoryParams, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/git-repositories") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). + queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string + + if params.Id != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "id", *params.Id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + } + + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") + } + + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewDeleteGitRepositoryRequest generates requests for DeleteGitRepository +func NewDeleteGitRepositoryRequest(server string, gitRepositoryId GitRepositoryIdPath) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "gitRepositoryId", gitRepositoryId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/git-repositories/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetGitRepositoryRequest generates requests for GetGitRepository +func NewGetGitRepositoryRequest(server string, gitRepositoryId GitRepositoryIdPath) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "gitRepositoryId", gitRepositoryId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/git-repositories/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewUpdateGitRepositoryRequest calls the generic UpdateGitRepository builder with application/json body +func NewUpdateGitRepositoryRequest(server string, gitRepositoryId GitRepositoryIdPath, body UpdateGitRepositoryJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewUpdateGitRepositoryRequestWithBody(server, gitRepositoryId, "application/json", bodyReader) +} + +// NewUpdateGitRepositoryRequestWithBody generates requests for UpdateGitRepository with any type of body +func NewUpdateGitRepositoryRequestWithBody(server string, gitRepositoryId GitRepositoryIdPath, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "gitRepositoryId", gitRepositoryId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/git-repositories/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPut, queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewGetGitRepositoryStatusRequest generates requests for GetGitRepositoryStatus +func NewGetGitRepositoryStatusRequest(server string, gitRepositoryId GitRepositoryIdPath) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "gitRepositoryId", gitRepositoryId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/git-repositories/%s/status", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewSyncGitRepositoryRequest generates requests for SyncGitRepository +func NewSyncGitRepositoryRequest(server string, gitRepositoryId GitRepositoryIdPath) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "gitRepositoryId", gitRepositoryId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/git-repositories/%s:sync", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // ListGitRepositoriesWithResponse request + ListGitRepositoriesWithResponse(ctx context.Context, params *ListGitRepositoriesParams, reqEditors ...RequestEditorFn) (*ListGitRepositoriesResponse, error) + + // CreateGitRepositoryWithBodyWithResponse request with any body + CreateGitRepositoryWithBodyWithResponse(ctx context.Context, params *CreateGitRepositoryParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateGitRepositoryResponse, error) + + CreateGitRepositoryWithResponse(ctx context.Context, params *CreateGitRepositoryParams, body CreateGitRepositoryJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateGitRepositoryResponse, error) + + // DeleteGitRepositoryWithResponse request + DeleteGitRepositoryWithResponse(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*DeleteGitRepositoryResponse, error) + + // GetGitRepositoryWithResponse request + GetGitRepositoryWithResponse(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*GetGitRepositoryResponse, error) + + // UpdateGitRepositoryWithBodyWithResponse request with any body + UpdateGitRepositoryWithBodyWithResponse(ctx context.Context, gitRepositoryId GitRepositoryIdPath, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpdateGitRepositoryResponse, error) + + UpdateGitRepositoryWithResponse(ctx context.Context, gitRepositoryId GitRepositoryIdPath, body UpdateGitRepositoryJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateGitRepositoryResponse, error) + + // GetGitRepositoryStatusWithResponse request + GetGitRepositoryStatusWithResponse(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*GetGitRepositoryStatusResponse, error) + + // SyncGitRepositoryWithResponse request + SyncGitRepositoryWithResponse(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*SyncGitRepositoryResponse, error) +} + +type ListGitRepositoriesResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *GitRepositoryList + JSON400 *BadRequest + JSON401 *Unauthorized + JSON403 *Forbidden + JSON500 *InternalServerError +} + +// Status returns HTTPResponse.Status +func (r ListGitRepositoriesResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ListGitRepositoriesResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r ListGitRepositoriesResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type CreateGitRepositoryResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *GitRepository + JSON400 *Error + JSON401 *Unauthorized + JSON403 *Forbidden + JSON409 *AlreadyExists + JSON500 *InternalServerError +} + +// Status returns HTTPResponse.Status +func (r CreateGitRepositoryResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r CreateGitRepositoryResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r CreateGitRepositoryResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type DeleteGitRepositoryResponse struct { + Body []byte + HTTPResponse *http.Response + JSON401 *Unauthorized + JSON403 *Forbidden + JSON404 *NotFound + JSON500 *InternalServerError +} + +// Status returns HTTPResponse.Status +func (r DeleteGitRepositoryResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteGitRepositoryResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r DeleteGitRepositoryResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type GetGitRepositoryResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *GitRepository + JSON401 *Unauthorized + JSON403 *Forbidden + JSON404 *NotFound + JSON500 *InternalServerError +} + +// Status returns HTTPResponse.Status +func (r GetGitRepositoryResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetGitRepositoryResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r GetGitRepositoryResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type UpdateGitRepositoryResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *GitRepository + JSON400 *Error + JSON401 *Unauthorized + JSON403 *Forbidden + JSON404 *NotFound + JSON500 *InternalServerError +} + +// Status returns HTTPResponse.Status +func (r UpdateGitRepositoryResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r UpdateGitRepositoryResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r UpdateGitRepositoryResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type GetGitRepositoryStatusResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *GitRepositoryStatus + JSON401 *Unauthorized + JSON403 *Forbidden + JSON404 *NotFound + JSON500 *InternalServerError +} + +// Status returns HTTPResponse.Status +func (r GetGitRepositoryStatusResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetGitRepositoryStatusResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r GetGitRepositoryStatusResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type SyncGitRepositoryResponse struct { + Body []byte + HTTPResponse *http.Response + JSON202 *GitRepositoryStatus + JSON401 *Unauthorized + JSON403 *Forbidden + JSON404 *NotFound + JSON500 *InternalServerError +} + +// Status returns HTTPResponse.Status +func (r SyncGitRepositoryResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r SyncGitRepositoryResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r SyncGitRepositoryResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +// ListGitRepositoriesWithResponse request returning *ListGitRepositoriesResponse +func (c *ClientWithResponses) ListGitRepositoriesWithResponse(ctx context.Context, params *ListGitRepositoriesParams, reqEditors ...RequestEditorFn) (*ListGitRepositoriesResponse, error) { + rsp, err := c.ListGitRepositories(ctx, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseListGitRepositoriesResponse(rsp) +} + +// CreateGitRepositoryWithBodyWithResponse request with arbitrary body returning *CreateGitRepositoryResponse +func (c *ClientWithResponses) CreateGitRepositoryWithBodyWithResponse(ctx context.Context, params *CreateGitRepositoryParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateGitRepositoryResponse, error) { + rsp, err := c.CreateGitRepositoryWithBody(ctx, params, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateGitRepositoryResponse(rsp) +} + +func (c *ClientWithResponses) CreateGitRepositoryWithResponse(ctx context.Context, params *CreateGitRepositoryParams, body CreateGitRepositoryJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateGitRepositoryResponse, error) { + rsp, err := c.CreateGitRepository(ctx, params, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateGitRepositoryResponse(rsp) +} + +// DeleteGitRepositoryWithResponse request returning *DeleteGitRepositoryResponse +func (c *ClientWithResponses) DeleteGitRepositoryWithResponse(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*DeleteGitRepositoryResponse, error) { + rsp, err := c.DeleteGitRepository(ctx, gitRepositoryId, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteGitRepositoryResponse(rsp) +} + +// GetGitRepositoryWithResponse request returning *GetGitRepositoryResponse +func (c *ClientWithResponses) GetGitRepositoryWithResponse(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*GetGitRepositoryResponse, error) { + rsp, err := c.GetGitRepository(ctx, gitRepositoryId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetGitRepositoryResponse(rsp) +} + +// UpdateGitRepositoryWithBodyWithResponse request with arbitrary body returning *UpdateGitRepositoryResponse +func (c *ClientWithResponses) UpdateGitRepositoryWithBodyWithResponse(ctx context.Context, gitRepositoryId GitRepositoryIdPath, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpdateGitRepositoryResponse, error) { + rsp, err := c.UpdateGitRepositoryWithBody(ctx, gitRepositoryId, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseUpdateGitRepositoryResponse(rsp) +} + +func (c *ClientWithResponses) UpdateGitRepositoryWithResponse(ctx context.Context, gitRepositoryId GitRepositoryIdPath, body UpdateGitRepositoryJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateGitRepositoryResponse, error) { + rsp, err := c.UpdateGitRepository(ctx, gitRepositoryId, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseUpdateGitRepositoryResponse(rsp) +} + +// GetGitRepositoryStatusWithResponse request returning *GetGitRepositoryStatusResponse +func (c *ClientWithResponses) GetGitRepositoryStatusWithResponse(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*GetGitRepositoryStatusResponse, error) { + rsp, err := c.GetGitRepositoryStatus(ctx, gitRepositoryId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetGitRepositoryStatusResponse(rsp) +} + +// SyncGitRepositoryWithResponse request returning *SyncGitRepositoryResponse +func (c *ClientWithResponses) SyncGitRepositoryWithResponse(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*SyncGitRepositoryResponse, error) { + rsp, err := c.SyncGitRepository(ctx, gitRepositoryId, reqEditors...) + if err != nil { + return nil, err + } + return ParseSyncGitRepositoryResponse(rsp) +} + +// ParseListGitRepositoriesResponse parses an HTTP response from a ListGitRepositoriesWithResponse call +func ParseListGitRepositoriesResponse(rsp *http.Response) (*ListGitRepositoriesResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ListGitRepositoriesResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest GitRepositoryList + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequest + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Unauthorized + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Forbidden + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalServerError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseCreateGitRepositoryResponse parses an HTTP response from a CreateGitRepositoryWithResponse call +func ParseCreateGitRepositoryResponse(rsp *http.Response) (*CreateGitRepositoryResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &CreateGitRepositoryResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest GitRepository + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Unauthorized + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Forbidden + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest AlreadyExists + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalServerError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseDeleteGitRepositoryResponse parses an HTTP response from a DeleteGitRepositoryWithResponse call +func ParseDeleteGitRepositoryResponse(rsp *http.Response) (*DeleteGitRepositoryResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteGitRepositoryResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Unauthorized + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Forbidden + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFound + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalServerError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseGetGitRepositoryResponse parses an HTTP response from a GetGitRepositoryWithResponse call +func ParseGetGitRepositoryResponse(rsp *http.Response) (*GetGitRepositoryResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetGitRepositoryResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest GitRepository + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Unauthorized + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Forbidden + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFound + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalServerError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseUpdateGitRepositoryResponse parses an HTTP response from a UpdateGitRepositoryWithResponse call +func ParseUpdateGitRepositoryResponse(rsp *http.Response) (*UpdateGitRepositoryResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &UpdateGitRepositoryResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest GitRepository + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Unauthorized + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Forbidden + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFound + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalServerError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseGetGitRepositoryStatusResponse parses an HTTP response from a GetGitRepositoryStatusWithResponse call +func ParseGetGitRepositoryStatusResponse(rsp *http.Response) (*GetGitRepositoryStatusResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetGitRepositoryStatusResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest GitRepositoryStatus + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Unauthorized + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Forbidden + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFound + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalServerError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseSyncGitRepositoryResponse parses an HTTP response from a SyncGitRepositoryWithResponse call +func ParseSyncGitRepositoryResponse(rsp *http.Response) (*SyncGitRepositoryResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &SyncGitRepositoryResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202: + var dest GitRepositoryStatus + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON202 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Unauthorized + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Forbidden + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFound + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalServerError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} From 97cf73e2c147431bcc742ec69db3d2deb7a9694d Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Thu, 9 Jul 2026 15:32:21 +0200 Subject: [PATCH 02/14] (gitops): Add GitRepository store layer with GORM model Implement the data persistence layer for GitRepository resources including CRUD operations, sync status updates, pagination, and unique constraint handling. Includes unit tests with SQLite. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Ondra Machacek --- internal/gitops/store/git_repository.go | 197 +++++++++++++++ internal/gitops/store/git_repository_test.go | 235 ++++++++++++++++++ internal/gitops/store/model/git_repository.go | 27 ++ internal/gitops/store/pagination.go | 42 ++++ internal/gitops/store/store.go | 32 +++ internal/gitops/store/suite_test.go | 13 + 6 files changed, 546 insertions(+) create mode 100644 internal/gitops/store/git_repository.go create mode 100644 internal/gitops/store/git_repository_test.go create mode 100644 internal/gitops/store/model/git_repository.go create mode 100644 internal/gitops/store/pagination.go create mode 100644 internal/gitops/store/store.go create mode 100644 internal/gitops/store/suite_test.go diff --git a/internal/gitops/store/git_repository.go b/internal/gitops/store/git_repository.go new file mode 100644 index 0000000..05230b0 --- /dev/null +++ b/internal/gitops/store/git_repository.go @@ -0,0 +1,197 @@ +package store + +import ( + "context" + "errors" + "strings" + "time" + + "github.com/dcm-project/control-plane/internal/gitops/store/model" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +var ( + ErrGitRepositoryNotFound = errors.New("git repository not found") + ErrGitRepositoryIDTaken = errors.New("git repository ID already taken") + ErrDisplayNameTaken = errors.New("display_name already taken") +) + +// GitRepositoryListOptions contains options for listing git repositories. +type GitRepositoryListOptions struct { + PageToken *string + PageSize int +} + +// GitRepositoryListResult contains the result of a List operation. +type GitRepositoryListResult struct { + GitRepositories model.GitRepositoryList + NextPageToken string +} + +type GitRepository interface { + List(ctx context.Context, opts *GitRepositoryListOptions) (*GitRepositoryListResult, error) + ListAll(ctx context.Context) (model.GitRepositoryList, error) + Get(ctx context.Context, id string) (*model.GitRepository, error) + Create(ctx context.Context, repo model.GitRepository) (*model.GitRepository, error) + Update(ctx context.Context, repo model.GitRepository) (*model.GitRepository, error) + Delete(ctx context.Context, id string) error + UpdateSyncStatus(ctx context.Context, id, syncState, statusMessage, lastSyncedCommit string) error +} + +type GitRepositoryStore struct { + db *gorm.DB +} + +var _ GitRepository = (*GitRepositoryStore)(nil) + +func NewGitRepository(db *gorm.DB) GitRepository { + return &GitRepositoryStore{db: db} +} + +func (s *GitRepositoryStore) List(ctx context.Context, opts *GitRepositoryListOptions) (*GitRepositoryListResult, error) { + var repos model.GitRepositoryList + query := s.db.WithContext(ctx) + + pageSize := 50 + if opts != nil && opts.PageSize > 0 { + pageSize = opts.PageSize + } + + offset := 0 + if opts != nil && opts.PageToken != nil && *opts.PageToken != "" { + var err error + offset, err = decodePageToken(*opts.PageToken) + if err != nil { + return nil, err + } + } + + query = query.Order("id ASC").Limit(pageSize + 1).Offset(offset) + + if err := query.Find(&repos).Error; err != nil { + return nil, err + } + + result := &GitRepositoryListResult{ + GitRepositories: repos, + } + + if len(repos) > pageSize { + result.GitRepositories = repos[:pageSize] + nextOffset := offset + pageSize + nextPageToken, err := encodePageToken(nextOffset) + if err != nil { + return nil, err + } + result.NextPageToken = nextPageToken + } + + return result, nil +} + +func (s *GitRepositoryStore) ListAll(ctx context.Context) (model.GitRepositoryList, error) { + var repos model.GitRepositoryList + if err := s.db.WithContext(ctx).Order("id ASC").Find(&repos).Error; err != nil { + return nil, err + } + if repos == nil { + repos = model.GitRepositoryList{} + } + return repos, nil +} + +func (s *GitRepositoryStore) Get(ctx context.Context, id string) (*model.GitRepository, error) { + var repo model.GitRepository + if err := s.db.WithContext(ctx).First(&repo, "id = ?", id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrGitRepositoryNotFound + } + return nil, err + } + return &repo, nil +} + +func (s *GitRepositoryStore) Create(ctx context.Context, repo model.GitRepository) (*model.GitRepository, error) { + if err := s.db.WithContext(ctx).Clauses(clause.Returning{}).Select("*").Create(&repo).Error; err != nil { + return nil, s.mapUniqueConstraintError(ctx, err, repo) + } + return &repo, nil +} + +func (s *GitRepositoryStore) Update(ctx context.Context, repo model.GitRepository) (*model.GitRepository, error) { + result := s.db.WithContext(ctx).Model(&repo). + Select("display_name", "url", "branch", "path", "interval_seconds", "max_retries", "backoff_seconds"). + Clauses(clause.Returning{}). + Updates(&repo) + if result.Error != nil { + return nil, s.mapUniqueConstraintError(ctx, result.Error, repo) + } + if result.RowsAffected == 0 { + return nil, ErrGitRepositoryNotFound + } + return &repo, nil +} + +func (s *GitRepositoryStore) Delete(ctx context.Context, id string) error { + result := s.db.WithContext(ctx).Where("id = ?", id).Delete(&model.GitRepository{}) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return ErrGitRepositoryNotFound + } + return nil +} + +func (s *GitRepositoryStore) UpdateSyncStatus(ctx context.Context, id, syncState, statusMessage, lastSyncedCommit string) error { + now := time.Now().UTC() + result := s.db.WithContext(ctx).Model(&model.GitRepository{}). + Where("id = ?", id). + Updates(map[string]any{ + "sync_state": syncState, + "status_message": statusMessage, + "last_synced_commit": lastSyncedCommit, + "last_sync_time": &now, + }) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return ErrGitRepositoryNotFound + } + return nil +} + +func (s *GitRepositoryStore) mapUniqueConstraintError(ctx context.Context, err error, attempted model.GitRepository) error { + if err == nil { + return nil + } + if !errors.Is(err, gorm.ErrDuplicatedKey) { + if !strings.Contains(strings.ToLower(err.Error()), "unique") && + !strings.Contains(err.Error(), "duplicate key") { + return err + } + } + + checks := []struct { + sentinel error + query *gorm.DB + }{ + {ErrGitRepositoryIDTaken, s.db.WithContext(ctx).Where("id = ?", attempted.ID).Limit(1)}, + {ErrDisplayNameTaken, s.db.WithContext(ctx).Where("display_name = ?", attempted.DisplayName).Limit(1)}, + } + + for _, c := range checks { + var row model.GitRepository + dberr := c.query.First(&row).Error + if dberr == nil { + return c.sentinel + } + if !errors.Is(dberr, gorm.ErrRecordNotFound) { + return err + } + } + + return err +} diff --git a/internal/gitops/store/git_repository_test.go b/internal/gitops/store/git_repository_test.go new file mode 100644 index 0000000..4a3449b --- /dev/null +++ b/internal/gitops/store/git_repository_test.go @@ -0,0 +1,235 @@ +package store_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + + "github.com/dcm-project/control-plane/internal/gitops/store" + "github.com/dcm-project/control-plane/internal/gitops/store/model" +) + +var _ = Describe("GitRepositoryStore", func() { + var ( + db *gorm.DB + dataStore store.Store + ctx context.Context + ) + + BeforeEach(func() { + var err error + db, err = gorm.Open(sqlite.Open(":memory:"), &gorm.Config{TranslateError: true}) + Expect(err).NotTo(HaveOccurred()) + Expect(db.AutoMigrate(&model.GitRepository{})).To(Succeed()) + dataStore = store.NewStore(db) + ctx = context.Background() + }) + + Describe("Create", func() { + It("creates a git repository", func() { + repo := model.GitRepository{ + ID: "test-repo", + ApiVersion: "v1alpha1", + DisplayName: "Test Repo", + URL: "https://example.com/repo.git", + Branch: "main", + Path: ".", + } + created, err := dataStore.GitRepository().Create(ctx, repo) + Expect(err).NotTo(HaveOccurred()) + Expect(created.ID).To(Equal("test-repo")) + Expect(created.DisplayName).To(Equal("Test Repo")) + Expect(created.SyncState).To(Equal("PENDING")) + }) + + It("returns error on duplicate ID", func() { + repo := model.GitRepository{ + ID: "dup-id", + ApiVersion: "v1alpha1", + DisplayName: "First", + URL: "https://example.com/repo.git", + } + _, err := dataStore.GitRepository().Create(ctx, repo) + Expect(err).NotTo(HaveOccurred()) + + repo2 := model.GitRepository{ + ID: "dup-id", + ApiVersion: "v1alpha1", + DisplayName: "Second", + URL: "https://example.com/repo2.git", + } + _, err = dataStore.GitRepository().Create(ctx, repo2) + Expect(err).To(MatchError(store.ErrGitRepositoryIDTaken)) + }) + + It("returns error on duplicate display_name", func() { + repo := model.GitRepository{ + ID: "repo-1", + ApiVersion: "v1alpha1", + DisplayName: "Same Name", + URL: "https://example.com/repo.git", + } + _, err := dataStore.GitRepository().Create(ctx, repo) + Expect(err).NotTo(HaveOccurred()) + + repo2 := model.GitRepository{ + ID: "repo-2", + ApiVersion: "v1alpha1", + DisplayName: "Same Name", + URL: "https://example.com/repo2.git", + } + _, err = dataStore.GitRepository().Create(ctx, repo2) + Expect(err).To(MatchError(store.ErrDisplayNameTaken)) + }) + }) + + Describe("Get", func() { + It("returns the repository", func() { + repo := model.GitRepository{ + ID: "get-test", + ApiVersion: "v1alpha1", + DisplayName: "Get Test", + URL: "https://example.com/repo.git", + } + _, err := dataStore.GitRepository().Create(ctx, repo) + Expect(err).NotTo(HaveOccurred()) + + got, err := dataStore.GitRepository().Get(ctx, "get-test") + Expect(err).NotTo(HaveOccurred()) + Expect(got.DisplayName).To(Equal("Get Test")) + }) + + It("returns not found for non-existent ID", func() { + _, err := dataStore.GitRepository().Get(ctx, "nonexistent") + Expect(err).To(MatchError(store.ErrGitRepositoryNotFound)) + }) + }) + + Describe("List", func() { + BeforeEach(func() { + for _, id := range []string{"aaa", "bbb", "ccc"} { + _, err := dataStore.GitRepository().Create(ctx, model.GitRepository{ + ID: id, + ApiVersion: "v1alpha1", + DisplayName: "Repo " + id, + URL: "https://example.com/" + id + ".git", + }) + Expect(err).NotTo(HaveOccurred()) + } + }) + + It("lists all repositories", func() { + result, err := dataStore.GitRepository().List(ctx, &store.GitRepositoryListOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(result.GitRepositories).To(HaveLen(3)) + Expect(result.NextPageToken).To(BeEmpty()) + }) + + It("supports pagination", func() { + result, err := dataStore.GitRepository().List(ctx, &store.GitRepositoryListOptions{PageSize: 2}) + Expect(err).NotTo(HaveOccurred()) + Expect(result.GitRepositories).To(HaveLen(2)) + Expect(result.NextPageToken).NotTo(BeEmpty()) + + result2, err := dataStore.GitRepository().List(ctx, &store.GitRepositoryListOptions{ + PageToken: &result.NextPageToken, + PageSize: 2, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(result2.GitRepositories).To(HaveLen(1)) + Expect(result2.NextPageToken).To(BeEmpty()) + }) + }) + + Describe("Delete", func() { + It("deletes an existing repository", func() { + _, err := dataStore.GitRepository().Create(ctx, model.GitRepository{ + ID: "del-test", + ApiVersion: "v1alpha1", + DisplayName: "Del Test", + URL: "https://example.com/repo.git", + }) + Expect(err).NotTo(HaveOccurred()) + + err = dataStore.GitRepository().Delete(ctx, "del-test") + Expect(err).NotTo(HaveOccurred()) + + _, err = dataStore.GitRepository().Get(ctx, "del-test") + Expect(err).To(MatchError(store.ErrGitRepositoryNotFound)) + }) + + It("returns not found for non-existent ID", func() { + err := dataStore.GitRepository().Delete(ctx, "nonexistent") + Expect(err).To(MatchError(store.ErrGitRepositoryNotFound)) + }) + }) + + Describe("UpdateSyncStatus", func() { + It("updates sync status fields", func() { + _, err := dataStore.GitRepository().Create(ctx, model.GitRepository{ + ID: "sync-test", + ApiVersion: "v1alpha1", + DisplayName: "Sync Test", + URL: "https://example.com/repo.git", + }) + Expect(err).NotTo(HaveOccurred()) + + err = dataStore.GitRepository().UpdateSyncStatus(ctx, "sync-test", "SYNCED", "All good", "abc123") + Expect(err).NotTo(HaveOccurred()) + + got, err := dataStore.GitRepository().Get(ctx, "sync-test") + Expect(err).NotTo(HaveOccurred()) + Expect(got.SyncState).To(Equal("SYNCED")) + Expect(got.StatusMessage).To(Equal("All good")) + Expect(got.LastSyncedCommit).To(Equal("abc123")) + Expect(got.LastSyncTime).NotTo(BeNil()) + }) + + It("returns not found for non-existent ID", func() { + err := dataStore.GitRepository().UpdateSyncStatus(ctx, "nonexistent", "SYNCED", "", "") + Expect(err).To(MatchError(store.ErrGitRepositoryNotFound)) + }) + }) + + Describe("Update", func() { + It("updates mutable fields", func() { + _, err := dataStore.GitRepository().Create(ctx, model.GitRepository{ + ID: "upd-test", + ApiVersion: "v1alpha1", + DisplayName: "Original", + URL: "https://example.com/repo.git", + Branch: "main", + IntervalSeconds: 60, + }) + Expect(err).NotTo(HaveOccurred()) + + updated, err := dataStore.GitRepository().Update(ctx, model.GitRepository{ + ID: "upd-test", + DisplayName: "Updated", + URL: "https://example.com/new-repo.git", + Branch: "develop", + Path: "apps/", + IntervalSeconds: 30, + MaxRetries: 5, + BackoffSeconds: 10, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(updated.DisplayName).To(Equal("Updated")) + Expect(updated.URL).To(Equal("https://example.com/new-repo.git")) + Expect(updated.Branch).To(Equal("develop")) + Expect(updated.IntervalSeconds).To(Equal(30)) + }) + + It("returns not found for non-existent ID", func() { + _, err := dataStore.GitRepository().Update(ctx, model.GitRepository{ + ID: "nonexistent", + DisplayName: "Test", + URL: "https://example.com/repo.git", + }) + Expect(err).To(MatchError(store.ErrGitRepositoryNotFound)) + }) + }) +}) diff --git a/internal/gitops/store/model/git_repository.go b/internal/gitops/store/model/git_repository.go new file mode 100644 index 0000000..f931768 --- /dev/null +++ b/internal/gitops/store/model/git_repository.go @@ -0,0 +1,27 @@ +package model + +import ( + "time" +) + +// GitRepository represents a git repository configuration in the database. +type GitRepository struct { + ID string `gorm:"column:id;primaryKey;type:varchar(63)"` + ApiVersion string `gorm:"column:api_version;not null"` + DisplayName string `gorm:"column:display_name;not null;uniqueIndex"` + URL string `gorm:"column:url;not null"` + Branch string `gorm:"column:branch;not null;default:main"` + Path string `gorm:"column:path;not null;default:."` + IntervalSeconds int `gorm:"column:interval_seconds;not null;default:60"` + MaxRetries int `gorm:"column:max_retries;not null;default:3"` + BackoffSeconds int `gorm:"column:backoff_seconds;not null;default:30"` + SyncState string `gorm:"column:sync_state;not null;default:PENDING"` + LastSyncedCommit string `gorm:"column:last_synced_commit"` + StatusMessage string `gorm:"column:status_message"` + LastSyncTime *time.Time `gorm:"column:last_sync_time"` + CreateTime time.Time `gorm:"column:create_time;autoCreateTime"` + UpdateTime time.Time `gorm:"column:update_time;autoUpdateTime"` +} + +// GitRepositoryList is a slice of GitRepository for list results. +type GitRepositoryList []GitRepository diff --git a/internal/gitops/store/pagination.go b/internal/gitops/store/pagination.go new file mode 100644 index 0000000..f5e6716 --- /dev/null +++ b/internal/gitops/store/pagination.go @@ -0,0 +1,42 @@ +package store + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" +) + +// ErrInvalidPageToken is returned when page_token cannot be decoded or validated. +var ErrInvalidPageToken = errors.New("store: invalid page_token") + +type pageTokenPayload struct { + Offset int `json:"offset"` +} + +func decodePageToken(token string) (int, error) { + decoded, err := base64.StdEncoding.DecodeString(token) + if err != nil { + return 0, fmt.Errorf("%w: %v", ErrInvalidPageToken, err) + } + + var payload pageTokenPayload + if err := json.Unmarshal(decoded, &payload); err != nil { + return 0, fmt.Errorf("%w: %v", ErrInvalidPageToken, err) + } + if payload.Offset < 0 { + return 0, fmt.Errorf("%w: negative offset", ErrInvalidPageToken) + } + return payload.Offset, nil +} + +func encodePageToken(offset int) (string, error) { + if offset < 0 { + return "", fmt.Errorf("%w: negative offset", ErrInvalidPageToken) + } + payload, err := json.Marshal(pageTokenPayload{Offset: offset}) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(payload), nil +} diff --git a/internal/gitops/store/store.go b/internal/gitops/store/store.go new file mode 100644 index 0000000..1efafda --- /dev/null +++ b/internal/gitops/store/store.go @@ -0,0 +1,32 @@ +package store + +import "gorm.io/gorm" + +type Store interface { + Close() error + GitRepository() GitRepository +} + +type DataStore struct { + db *gorm.DB + gitRepository GitRepository +} + +func NewStore(db *gorm.DB) Store { + return &DataStore{ + db: db, + gitRepository: NewGitRepository(db), + } +} + +func (s *DataStore) Close() error { + sqlDB, err := s.db.DB() + if err != nil { + return err + } + return sqlDB.Close() +} + +func (s *DataStore) GitRepository() GitRepository { + return s.gitRepository +} diff --git a/internal/gitops/store/suite_test.go b/internal/gitops/store/suite_test.go new file mode 100644 index 0000000..0879728 --- /dev/null +++ b/internal/gitops/store/suite_test.go @@ -0,0 +1,13 @@ +package store_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestStore(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "GitOps Store Suite") +} From 9dbc5a723760dbc8c92ada57fc424b80bab093fb Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Thu, 9 Jul 2026 15:32:29 +0200 Subject: [PATCH 03/14] (gitops): Add GitRepository service and HTTP handlers Implement the business logic and HTTP handler layers for the GitRepository API. The service handles validation and API-to-model conversion; handlers implement the generated StrictServerInterface. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Ondra Machacek --- internal/gitops/handlers/v1alpha1/errors.go | 135 ++++++++++ internal/gitops/handlers/v1alpha1/handler.go | 98 ++++++++ internal/gitops/service/service.go | 248 +++++++++++++++++++ 3 files changed, 481 insertions(+) create mode 100644 internal/gitops/handlers/v1alpha1/errors.go create mode 100644 internal/gitops/handlers/v1alpha1/handler.go create mode 100644 internal/gitops/service/service.go diff --git a/internal/gitops/handlers/v1alpha1/errors.go b/internal/gitops/handlers/v1alpha1/errors.go new file mode 100644 index 0000000..0a6d4d8 --- /dev/null +++ b/internal/gitops/handlers/v1alpha1/errors.go @@ -0,0 +1,135 @@ +package v1alpha1 + +import ( + "errors" + + v1alpha1 "github.com/dcm-project/control-plane/api/gitops/v1alpha1" + "github.com/dcm-project/control-plane/internal/gitops/api/server" + "github.com/dcm-project/control-plane/internal/gitops/service" +) + +func buildError(status int32, errType v1alpha1.ErrorType, title, detail string) v1alpha1.Error { + return v1alpha1.Error{ + Status: status, + Type: errType, + Title: title, + Detail: &detail, + } +} + +func handleListError(err error) server.ListGitRepositoriesResponseObject { + if errors.Is(err, service.ErrInvalidArgument) { + return server.ListGitRepositories400JSONResponse{ + BadRequestJSONResponse: server.BadRequestJSONResponse(buildError( + 400, v1alpha1.INVALIDARGUMENT, "Invalid request", err.Error(), + )), + } + } + return server.ListGitRepositories500JSONResponse{ + InternalServerErrorJSONResponse: server.InternalServerErrorJSONResponse(buildError( + 500, v1alpha1.INTERNAL, "Internal server error", err.Error(), + )), + } +} + +func handleCreateError(err error) server.CreateGitRepositoryResponseObject { + switch { + case errors.Is(err, service.ErrInvalidArgument): + return server.CreateGitRepository400JSONResponse(buildError( + 400, v1alpha1.INVALIDARGUMENT, "Invalid request", err.Error(), + )) + case errors.Is(err, service.ErrAlreadyExists): + return server.CreateGitRepository409JSONResponse{ + AlreadyExistsJSONResponse: server.AlreadyExistsJSONResponse(buildError( + 409, v1alpha1.ALREADYEXISTS, "Resource already exists", err.Error(), + )), + } + default: + return server.CreateGitRepository500JSONResponse{ + InternalServerErrorJSONResponse: server.InternalServerErrorJSONResponse(buildError( + 500, v1alpha1.INTERNAL, "Internal server error", err.Error(), + )), + } + } +} + +func handleGetError(err error) server.GetGitRepositoryResponseObject { + if errors.Is(err, service.ErrNotFound) { + return server.GetGitRepository404JSONResponse{ + NotFoundJSONResponse: server.NotFoundJSONResponse(buildError( + 404, v1alpha1.NOTFOUND, "Resource not found", err.Error(), + )), + } + } + return server.GetGitRepository500JSONResponse{ + InternalServerErrorJSONResponse: server.InternalServerErrorJSONResponse(buildError( + 500, v1alpha1.INTERNAL, "Internal server error", err.Error(), + )), + } +} + +func handleUpdateError(err error) server.UpdateGitRepositoryResponseObject { + switch { + case errors.Is(err, service.ErrInvalidArgument): + return server.UpdateGitRepository400JSONResponse(buildError( + 400, v1alpha1.INVALIDARGUMENT, "Invalid request", err.Error(), + )) + case errors.Is(err, service.ErrNotFound): + return server.UpdateGitRepository404JSONResponse{ + NotFoundJSONResponse: server.NotFoundJSONResponse(buildError( + 404, v1alpha1.NOTFOUND, "Resource not found", err.Error(), + )), + } + default: + return server.UpdateGitRepository500JSONResponse{ + InternalServerErrorJSONResponse: server.InternalServerErrorJSONResponse(buildError( + 500, v1alpha1.INTERNAL, "Internal server error", err.Error(), + )), + } + } +} + +func handleDeleteError(err error) server.DeleteGitRepositoryResponseObject { + if errors.Is(err, service.ErrNotFound) { + return server.DeleteGitRepository404JSONResponse{ + NotFoundJSONResponse: server.NotFoundJSONResponse(buildError( + 404, v1alpha1.NOTFOUND, "Resource not found", err.Error(), + )), + } + } + return server.DeleteGitRepository500JSONResponse{ + InternalServerErrorJSONResponse: server.InternalServerErrorJSONResponse(buildError( + 500, v1alpha1.INTERNAL, "Internal server error", err.Error(), + )), + } +} + +func handleGetStatusError(err error) server.GetGitRepositoryStatusResponseObject { + if errors.Is(err, service.ErrNotFound) { + return server.GetGitRepositoryStatus404JSONResponse{ + NotFoundJSONResponse: server.NotFoundJSONResponse(buildError( + 404, v1alpha1.NOTFOUND, "Resource not found", err.Error(), + )), + } + } + return server.GetGitRepositoryStatus500JSONResponse{ + InternalServerErrorJSONResponse: server.InternalServerErrorJSONResponse(buildError( + 500, v1alpha1.INTERNAL, "Internal server error", err.Error(), + )), + } +} + +func handleSyncError(err error) server.SyncGitRepositoryResponseObject { + if errors.Is(err, service.ErrNotFound) { + return server.SyncGitRepository404JSONResponse{ + NotFoundJSONResponse: server.NotFoundJSONResponse(buildError( + 404, v1alpha1.NOTFOUND, "Resource not found", err.Error(), + )), + } + } + return server.SyncGitRepository500JSONResponse{ + InternalServerErrorJSONResponse: server.InternalServerErrorJSONResponse(buildError( + 500, v1alpha1.INTERNAL, "Internal server error", err.Error(), + )), + } +} diff --git a/internal/gitops/handlers/v1alpha1/handler.go b/internal/gitops/handlers/v1alpha1/handler.go new file mode 100644 index 0000000..6976be9 --- /dev/null +++ b/internal/gitops/handlers/v1alpha1/handler.go @@ -0,0 +1,98 @@ +// Package v1alpha1 handles v1alpha1 API requests for GitRepository CRUD operations. +package v1alpha1 + +import ( + "context" + "log/slog" + + v1alpha1 "github.com/dcm-project/control-plane/api/gitops/v1alpha1" + "github.com/dcm-project/control-plane/internal/gitops/api/server" + "github.com/dcm-project/control-plane/internal/gitops/service" +) + +type Handler struct { + service service.GitRepositoryService +} + +// Ensure Handler implements StrictServerInterface +var _ server.StrictServerInterface = (*Handler)(nil) + +func NewHandler(svc service.GitRepositoryService) *Handler { + return &Handler{service: svc} +} + +func (h *Handler) ListGitRepositories(ctx context.Context, request server.ListGitRepositoriesRequestObject) (server.ListGitRepositoriesResponseObject, error) { + result, err := h.service.List(ctx, request.Params.PageToken, request.Params.MaxPageSize) + if err != nil { + return handleListError(err), nil + } + return server.ListGitRepositories200JSONResponse(*result), nil +} + +func (h *Handler) CreateGitRepository(ctx context.Context, request server.CreateGitRepositoryRequestObject) (server.CreateGitRepositoryResponseObject, error) { + if request.Body == nil { + return server.CreateGitRepository400JSONResponse(buildError( + 400, v1alpha1.INVALIDARGUMENT, "Invalid request body", "Request body is required", + )), nil + } + + created, err := h.service.Create(ctx, *request.Body, request.Params.Id) + if err != nil { + return handleCreateError(err), nil + } + + slog.InfoContext(ctx, "GitRepository created", "id", *created.Uid) + return server.CreateGitRepository201JSONResponse(*created), nil +} + +func (h *Handler) GetGitRepository(ctx context.Context, request server.GetGitRepositoryRequestObject) (server.GetGitRepositoryResponseObject, error) { + repo, err := h.service.Get(ctx, request.GitRepositoryId) + if err != nil { + return handleGetError(err), nil + } + return server.GetGitRepository200JSONResponse(*repo), nil +} + +func (h *Handler) UpdateGitRepository(ctx context.Context, request server.UpdateGitRepositoryRequestObject) (server.UpdateGitRepositoryResponseObject, error) { + if request.Body == nil { + return server.UpdateGitRepository400JSONResponse(buildError( + 400, v1alpha1.INVALIDARGUMENT, "Invalid request body", "Request body is required", + )), nil + } + + updated, err := h.service.Update(ctx, request.GitRepositoryId, *request.Body) + if err != nil { + return handleUpdateError(err), nil + } + + slog.InfoContext(ctx, "GitRepository updated", "id", request.GitRepositoryId) + return server.UpdateGitRepository200JSONResponse(*updated), nil +} + +func (h *Handler) DeleteGitRepository(ctx context.Context, request server.DeleteGitRepositoryRequestObject) (server.DeleteGitRepositoryResponseObject, error) { + err := h.service.Delete(ctx, request.GitRepositoryId) + if err != nil { + return handleDeleteError(err), nil + } + + slog.InfoContext(ctx, "GitRepository deleted", "id", request.GitRepositoryId) + return server.DeleteGitRepository204Response{}, nil +} + +func (h *Handler) GetGitRepositoryStatus(ctx context.Context, request server.GetGitRepositoryStatusRequestObject) (server.GetGitRepositoryStatusResponseObject, error) { + status, err := h.service.GetStatus(ctx, request.GitRepositoryId) + if err != nil { + return handleGetStatusError(err), nil + } + return server.GetGitRepositoryStatus200JSONResponse(*status), nil +} + +func (h *Handler) SyncGitRepository(ctx context.Context, request server.SyncGitRepositoryRequestObject) (server.SyncGitRepositoryResponseObject, error) { + status, err := h.service.TriggerSync(ctx, request.GitRepositoryId) + if err != nil { + return handleSyncError(err), nil + } + + slog.InfoContext(ctx, "GitRepository sync triggered", "id", request.GitRepositoryId) + return server.SyncGitRepository202JSONResponse(*status), nil +} diff --git a/internal/gitops/service/service.go b/internal/gitops/service/service.go new file mode 100644 index 0000000..4440287 --- /dev/null +++ b/internal/gitops/service/service.go @@ -0,0 +1,248 @@ +package service + +import ( + "context" + "errors" + "fmt" + + v1alpha1 "github.com/dcm-project/control-plane/api/gitops/v1alpha1" + "github.com/dcm-project/control-plane/internal/gitops/store" + "github.com/dcm-project/control-plane/internal/gitops/store/model" + "github.com/google/uuid" +) + +var ( + ErrNotFound = errors.New("git repository not found") + ErrAlreadyExists = errors.New("git repository already exists") + ErrInvalidArgument = errors.New("invalid argument") +) + +type GitRepositoryService interface { + List(ctx context.Context, pageToken *string, maxPageSize *int32) (*v1alpha1.GitRepositoryList, error) + Get(ctx context.Context, id string) (*v1alpha1.GitRepository, error) + Create(ctx context.Context, req v1alpha1.GitRepository, clientID *string) (*v1alpha1.GitRepository, error) + Update(ctx context.Context, id string, req v1alpha1.GitRepository) (*v1alpha1.GitRepository, error) + Delete(ctx context.Context, id string) error + GetStatus(ctx context.Context, id string) (*v1alpha1.GitRepositoryStatus, error) + TriggerSync(ctx context.Context, id string) (*v1alpha1.GitRepositoryStatus, error) +} + +type gitRepositoryService struct { + store store.Store +} + +func NewGitRepositoryService(store store.Store) GitRepositoryService { + return &gitRepositoryService{store: store} +} + +func (s *gitRepositoryService) List(ctx context.Context, pageToken *string, maxPageSize *int32) (*v1alpha1.GitRepositoryList, error) { + opts := &store.GitRepositoryListOptions{ + PageToken: pageToken, + } + if maxPageSize != nil { + opts.PageSize = int(*maxPageSize) + } + + result, err := s.store.GitRepository().List(ctx, opts) + if err != nil { + return nil, err + } + + apiTypes := make([]v1alpha1.GitRepository, len(result.GitRepositories)) + for i, m := range result.GitRepositories { + apiTypes[i] = modelToAPI(&m) + } + + return &v1alpha1.GitRepositoryList{ + Results: apiTypes, + NextPageToken: result.NextPageToken, + }, nil +} + +func (s *gitRepositoryService) Get(ctx context.Context, id string) (*v1alpha1.GitRepository, error) { + repo, err := s.store.GitRepository().Get(ctx, id) + if err != nil { + return nil, mapStoreError(err) + } + result := modelToAPI(repo) + return &result, nil +} + +func (s *gitRepositoryService) Create(ctx context.Context, req v1alpha1.GitRepository, clientID *string) (*v1alpha1.GitRepository, error) { + id := getOrGenerateID(clientID) + + if req.Spec.Url == "" { + return nil, fmt.Errorf("%w: spec.url is required", ErrInvalidArgument) + } + + m := apiToModel(id, req) + created, err := s.store.GitRepository().Create(ctx, m) + if err != nil { + return nil, mapStoreError(err) + } + result := modelToAPI(created) + return &result, nil +} + +func (s *gitRepositoryService) Update(ctx context.Context, id string, req v1alpha1.GitRepository) (*v1alpha1.GitRepository, error) { + if req.Spec.Url == "" { + return nil, fmt.Errorf("%w: spec.url is required", ErrInvalidArgument) + } + + m := apiToModel(id, req) + updated, err := s.store.GitRepository().Update(ctx, m) + if err != nil { + return nil, mapStoreError(err) + } + result := modelToAPI(updated) + return &result, nil +} + +func (s *gitRepositoryService) Delete(ctx context.Context, id string) error { + err := s.store.GitRepository().Delete(ctx, id) + if err != nil { + return mapStoreError(err) + } + return nil +} + +func (s *gitRepositoryService) GetStatus(ctx context.Context, id string) (*v1alpha1.GitRepositoryStatus, error) { + repo, err := s.store.GitRepository().Get(ctx, id) + if err != nil { + return nil, mapStoreError(err) + } + status := modelToStatus(repo) + return &status, nil +} + +func (s *gitRepositoryService) TriggerSync(ctx context.Context, id string) (*v1alpha1.GitRepositoryStatus, error) { + // Verify the repo exists + repo, err := s.store.GitRepository().Get(ctx, id) + if err != nil { + return nil, mapStoreError(err) + } + // The actual sync is performed by dcm-gitops; here we just return current status. + // A future enhancement can notify the controller via a channel. + status := modelToStatus(repo) + return &status, nil +} + +func getOrGenerateID(clientID *string) string { + if clientID != nil && *clientID != "" { + return *clientID + } + return uuid.New().String() +} + +func modelToAPI(m *model.GitRepository) v1alpha1.GitRepository { + path := fmt.Sprintf("git-repositories/%s", m.ID) + status := modelToStatus(m) + + repo := v1alpha1.GitRepository{ + Uid: &m.ID, + ApiVersion: m.ApiVersion, + DisplayName: m.DisplayName, + Spec: v1alpha1.GitRepositorySpec{ + Url: m.URL, + }, + Status: &status, + Path: &path, + CreateTime: &m.CreateTime, + UpdateTime: &m.UpdateTime, + } + + branch := m.Branch + repo.Spec.Ref = &struct { + Branch *string `json:"branch,omitempty"` + }{Branch: &branch} + + specPath := m.Path + repo.Spec.Path = &specPath + + interval := int32(m.IntervalSeconds) + repo.Spec.IntervalSeconds = &interval + + maxRetries := int32(m.MaxRetries) + backoff := int32(m.BackoffSeconds) + repo.Spec.Reconciliation = &struct { + RetryPolicy *struct { + BackoffSeconds *int32 `json:"backoff_seconds,omitempty"` + MaxRetries *int32 `json:"max_retries,omitempty"` + } `json:"retry_policy,omitempty"` + }{ + RetryPolicy: &struct { + BackoffSeconds *int32 `json:"backoff_seconds,omitempty"` + MaxRetries *int32 `json:"max_retries,omitempty"` + }{ + MaxRetries: &maxRetries, + BackoffSeconds: &backoff, + }, + } + + return repo +} + +func modelToStatus(m *model.GitRepository) v1alpha1.GitRepositoryStatus { + syncState := v1alpha1.GitRepositoryStatusSyncState(m.SyncState) + status := v1alpha1.GitRepositoryStatus{ + SyncState: &syncState, + LastSyncTime: m.LastSyncTime, + } + if m.LastSyncedCommit != "" { + status.LastSyncedCommit = &m.LastSyncedCommit + } + if m.StatusMessage != "" { + status.Message = &m.StatusMessage + } + return status +} + +func apiToModel(id string, req v1alpha1.GitRepository) model.GitRepository { + m := model.GitRepository{ + ID: id, + ApiVersion: req.ApiVersion, + DisplayName: req.DisplayName, + URL: req.Spec.Url, + Branch: "main", + Path: ".", + IntervalSeconds: 60, + MaxRetries: 3, + BackoffSeconds: 30, + SyncState: "PENDING", + } + + if req.Spec.Ref != nil && req.Spec.Ref.Branch != nil { + m.Branch = *req.Spec.Ref.Branch + } + if req.Spec.Path != nil { + m.Path = *req.Spec.Path + } + if req.Spec.IntervalSeconds != nil { + m.IntervalSeconds = int(*req.Spec.IntervalSeconds) + } + if req.Spec.Reconciliation != nil && req.Spec.Reconciliation.RetryPolicy != nil { + if req.Spec.Reconciliation.RetryPolicy.MaxRetries != nil { + m.MaxRetries = int(*req.Spec.Reconciliation.RetryPolicy.MaxRetries) + } + if req.Spec.Reconciliation.RetryPolicy.BackoffSeconds != nil { + m.BackoffSeconds = int(*req.Spec.Reconciliation.RetryPolicy.BackoffSeconds) + } + } + + return m +} + +func mapStoreError(err error) error { + switch { + case errors.Is(err, store.ErrGitRepositoryNotFound): + return fmt.Errorf("%w: %s", ErrNotFound, err.Error()) + case errors.Is(err, store.ErrGitRepositoryIDTaken): + return fmt.Errorf("%w: ID already taken", ErrAlreadyExists) + case errors.Is(err, store.ErrDisplayNameTaken): + return fmt.Errorf("%w: display_name already taken", ErrAlreadyExists) + case errors.Is(err, store.ErrInvalidPageToken): + return fmt.Errorf("%w: %s", ErrInvalidArgument, err.Error()) + default: + return err + } +} From c2f00d7fb5aacc04234215b0ad6a85891ca8b013 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Thu, 9 Jul 2026 15:32:38 +0200 Subject: [PATCH 04/14] (gitops): Wire GitRepository API into dcm-server Register GitRepository model in AutoMigrate, mount routes on the chi router, add OpenAPI request validation, and create build-gitops Makefile target. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Ondra Machacek --- Makefile | 10 +++++++--- internal/app/db.go | 2 ++ internal/app/openapi.go | 10 ++++++++++ internal/app/run.go | 14 ++++++++++++++ 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 11cb62a..718fc1e 100644 --- a/Makefile +++ b/Makefile @@ -33,11 +33,15 @@ include make/placement.mk include make/policy.mk include make/sp.mk include make/agent.mk +include make/gitops.mk # Same as Containerfile: static build, no CGO (Postgres in prod/compose). # For SQLite local dev use make run (go run with CGO). build: - CGO_ENABLED=0 go build -buildvcs=false -o bin/$(BINARY_NAME) ./cmd/$(BINARY_NAME) + CGO_ENABLED=1 go build -buildvcs=false -o bin/$(BINARY_NAME) ./cmd/$(BINARY_NAME) + +build-gitops: + CGO_ENABLED=0 go build -buildvcs=false -o bin/dcm-gitops ./cmd/dcm-gitops # Quick local start with SQLite (no Postgres/NATS stack required). # Uses a throwaway file under /tmp; override DB_NAME if you want a different path. @@ -112,6 +116,6 @@ test: tidy: go mod tidy -.PHONY: build run run-dev compose-up compose-up-with-providers compose-down image-build \ - clean fmt vet lint test test-catalog test-placement test-policy test-sp tidy \ +.PHONY: build build-gitops run run-dev compose-up compose-up-with-providers compose-down image-build \ + clean fmt vet lint test test-catalog test-placement test-policy test-sp test-gitops tidy \ helm-chart-sync helm-chart-verify-sync helm-chart-verify-admin-subject helm-chart-verify helm-chart-verify-schema helm-chart-lint helm-chart-template helm-chart-check diff --git a/internal/app/db.go b/internal/app/db.go index bbe110e..7426177 100644 --- a/internal/app/db.go +++ b/internal/app/db.go @@ -9,6 +9,7 @@ import ( agentmodel "github.com/dcm-project/control-plane/internal/agent/store/model" authmodel "github.com/dcm-project/control-plane/internal/auth/store/model" catalogmodel "github.com/dcm-project/control-plane/internal/catalog/store/model" + gitopsmodel "github.com/dcm-project/control-plane/internal/gitops/store/model" placementmodel "github.com/dcm-project/control-plane/internal/placement/store/model" policymodel "github.com/dcm-project/control-plane/internal/policy/store/model" spmodel "github.com/dcm-project/control-plane/internal/sp/store/model" @@ -76,6 +77,7 @@ func openDB(cfg *Config) (*gorm.DB, error) { &catalogmodel.ServiceType{}, &catalogmodel.CatalogItem{}, &catalogmodel.CatalogItemInstance{}, + &gitopsmodel.GitRepository{}, &placementmodel.Resource{}, &policymodel.Policy{}, &spmodel.ServiceTypeInstance{}, diff --git a/internal/app/openapi.go b/internal/app/openapi.go index 7fb7870..f036d9a 100644 --- a/internal/app/openapi.go +++ b/internal/app/openapi.go @@ -12,6 +12,7 @@ import ( agentapi "github.com/dcm-project/control-plane/api/agent/v1alpha1" catalogapi "github.com/dcm-project/control-plane/api/catalog/v1alpha1" + gitopsapi "github.com/dcm-project/control-plane/api/gitops/v1alpha1" policyapi "github.com/dcm-project/control-plane/api/policy/v1alpha1" sprmapi "github.com/dcm-project/control-plane/api/sp/v1alpha1/resource_manager" "github.com/getkin/kin-openapi/openapi3" @@ -24,6 +25,7 @@ const apiV1Alpha1Prefix = "/api/v1alpha1" type openAPIValidators struct { agent func(http.Handler) http.Handler catalog func(http.Handler) http.Handler + gitops func(http.Handler) http.Handler policy func(http.Handler) http.Handler rm func(http.Handler) http.Handler } @@ -39,6 +41,11 @@ func newOpenAPIValidators() (*openAPIValidators, error) { return nil, fmt.Errorf("load catalog OpenAPI spec: %w", err) } + gitopsSpec, err := gitopsapi.GetSpec() + if err != nil { + return nil, fmt.Errorf("load gitops OpenAPI spec: %w", err) + } + policySpec, err := policyapi.GetSpec() if err != nil { return nil, fmt.Errorf("load policy OpenAPI spec: %w", err) @@ -52,6 +59,7 @@ func newOpenAPIValidators() (*openAPIValidators, error) { return &openAPIValidators{ agent: oapiRequestValidator(agentSpec), catalog: oapiRequestValidator(catalogSpec), + gitops: oapiRequestValidator(gitopsSpec), policy: oapiRequestValidator(policySpec), rm: oapiRequestValidator(rmSpec), }, nil @@ -119,6 +127,8 @@ func (v *openAPIValidators) middleware() func(http.Handler) http.Handler { v.rm(next).ServeHTTP(w, r) case strings.HasPrefix(path, apiV1Alpha1Prefix+"/policies"): v.policy(next).ServeHTTP(w, r) + case strings.HasPrefix(path, apiV1Alpha1Prefix+"/git-repositories"): + v.gitops(next).ServeHTTP(w, r) default: v.catalog(next).ServeHTTP(w, r) } diff --git a/internal/app/run.go b/internal/app/run.go index ec3a4df..9329c03 100644 --- a/internal/app/run.go +++ b/internal/app/run.go @@ -26,6 +26,10 @@ import ( catalogplacement "github.com/dcm-project/control-plane/internal/catalog/placement" catalogservice "github.com/dcm-project/control-plane/internal/catalog/service" catalogstore "github.com/dcm-project/control-plane/internal/catalog/store" + gitopsserver "github.com/dcm-project/control-plane/internal/gitops/api/server" + gitopshandlers "github.com/dcm-project/control-plane/internal/gitops/handlers/v1alpha1" + gitopsservice "github.com/dcm-project/control-plane/internal/gitops/service" + gitopsstore "github.com/dcm-project/control-plane/internal/gitops/store" placementagent "github.com/dcm-project/control-plane/internal/placement/agent" placementlogging "github.com/dcm-project/control-plane/internal/placement/logging" placementpolicy "github.com/dcm-project/control-plane/internal/placement/policy" @@ -88,6 +92,7 @@ func Run() int { authSvc := authservice.NewService(authDataStore, cfg.Auth.AdminSubject, logger) catalogDataStore := catalogstore.NewStore(db, logger) + gitopsDataStore := gitopsstore.NewStore(db) policyDataStore := policystore.NewStore(db) placementDataStore := placementstore.NewStore(db) spDataStore := spstore.NewStore(db) @@ -250,9 +255,12 @@ func Run() int { authMiddleware = auth.Middleware(mwCfg) } + gitopsSvc := gitopsservice.NewGitRepositoryService(gitopsDataStore) + router, err := newRouter(authMiddleware, RouteHandlers{ Agent: agenthandlers.NewHandler(agentSvc), Catalog: cataloghandlers.NewHandler(catalogSvc, logger), + Gitops: gitopshandlers.NewHandler(gitopsSvc), Policy: policyhandlers.NewPolicyHandler(policyService), SPRM: sprmhandler.NewHandler(spInstanceService), }, checkers...) @@ -296,6 +304,7 @@ func Run() int { type RouteHandlers struct { Agent agentserver.StrictServerInterface Catalog catalogserver.StrictServerInterface + Gitops gitopsserver.StrictServerInterface Policy policyserver.StrictServerInterface SPRM sprmserver.StrictServerInterface } @@ -331,6 +340,11 @@ func newRouter(authMW func(http.Handler) http.Handler, h RouteHandlers, checkers router, baseURL, ) + gitopsserver.HandlerFromMuxWithBaseURL( + gitopsserver.NewStrictHandler(h.Gitops, nil), + router, + baseURL, + ) apiRouter := chi.NewRouter() sprmserver.HandlerFromMux( From f6f63929a6eae4e314bf9b06f3a16d57613e7b6a Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Thu, 9 Jul 2026 15:32:49 +0200 Subject: [PATCH 05/14] (gitops): Add GitOps controller with parser, reconciler, and git ops Implement the reconciliation engine that polls Git repositories and syncs CatalogItemInstance lifecycle state. Includes YAML parser for non-recursive file discovery, git clone/fetch via CLI, reconciler that classifies create/delete operations, and a controller loop with per-repo interval tracking. Includes parser unit tests. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Ondra Machacek --- internal/gitops/controller/controller.go | 94 ++++++++ internal/gitops/controller/parser.go | 156 ++++++++++++++ internal/gitops/controller/parser_test.go | 249 ++++++++++++++++++++++ internal/gitops/controller/reconciler.go | 189 ++++++++++++++++ internal/gitops/controller/repository.go | 78 +++++++ internal/gitops/controller/suite_test.go | 13 ++ 6 files changed, 779 insertions(+) create mode 100644 internal/gitops/controller/controller.go create mode 100644 internal/gitops/controller/parser.go create mode 100644 internal/gitops/controller/parser_test.go create mode 100644 internal/gitops/controller/reconciler.go create mode 100644 internal/gitops/controller/repository.go create mode 100644 internal/gitops/controller/suite_test.go diff --git a/internal/gitops/controller/controller.go b/internal/gitops/controller/controller.go new file mode 100644 index 0000000..31166ad --- /dev/null +++ b/internal/gitops/controller/controller.go @@ -0,0 +1,94 @@ +// Package controller implements the GitOps reconciliation loop. +package controller + +import ( + "context" + "log/slog" + "sync" + "time" + + "github.com/dcm-project/control-plane/internal/gitops/store" +) + +// Controller manages per-GitRepository reconciliation goroutines. +type Controller struct { + reconciler *Reconciler + store store.Store + pollInterval time.Duration // how often to reload repos from DB + stopCh chan struct{} + wg sync.WaitGroup +} + +// NewController creates a new Controller. +// pollInterval controls how often the controller reloads the list of GitRepositories from the DB. +func NewController(reconciler *Reconciler, gitopsStore store.Store, pollInterval time.Duration) *Controller { + return &Controller{ + reconciler: reconciler, + store: gitopsStore, + pollInterval: pollInterval, + stopCh: make(chan struct{}), + } +} + +// Start begins the main controller loop. +func (c *Controller) Start(ctx context.Context) { + c.wg.Add(1) + go c.run(ctx) +} + +// Stop gracefully stops the controller and waits for all goroutines to finish. +func (c *Controller) Stop() { + close(c.stopCh) + c.wg.Wait() +} + +func (c *Controller) run(ctx context.Context) { + defer c.wg.Done() + + // Run immediately on start, then on ticker + c.reconcileAll(ctx) + + ticker := time.NewTicker(c.pollInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-c.stopCh: + return + case <-ticker.C: + c.reconcileAll(ctx) + } + } +} + +func (c *Controller) reconcileAll(ctx context.Context) { + repos, err := c.store.GitRepository().ListAll(ctx) + if err != nil { + slog.ErrorContext(ctx, "Failed to list git repositories", "error", err) + return + } + + for _, repo := range repos { + select { + case <-ctx.Done(): + return + case <-c.stopCh: + return + default: + } + + // Check if it's time to reconcile based on interval + if repo.LastSyncTime != nil { + nextSync := repo.LastSyncTime.Add(time.Duration(repo.IntervalSeconds) * time.Second) + if time.Now().Before(nextSync) { + continue + } + } + + if err := c.reconciler.Reconcile(ctx, repo); err != nil { + slog.ErrorContext(ctx, "Reconciliation failed", "repo_id", repo.ID, "error", err) + } + } +} diff --git a/internal/gitops/controller/parser.go b/internal/gitops/controller/parser.go new file mode 100644 index 0000000..0715c76 --- /dev/null +++ b/internal/gitops/controller/parser.go @@ -0,0 +1,156 @@ +package controller + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "gopkg.in/yaml.v3" +) + +// DesiredInstance represents a CatalogItemInstance parsed from a YAML file. +type DesiredInstance struct { + Name string `yaml:"-"` + ApiVersion string `yaml:"-"` + DisplayName string `yaml:"-"` + CatalogItemID string `yaml:"-"` + UserValues []DesiredUserValue `yaml:"-"` + SourceFile string `yaml:"-"` + Labels map[string]string `yaml:"-"` +} + +// DesiredUserValue represents a user value in a desired instance YAML. +type DesiredUserValue struct { + Resource string `yaml:"resource" json:"resource"` + Path string `yaml:"path" json:"path"` + Value any `yaml:"value" json:"value"` +} + +// yamlDocument is the raw YAML structure we parse. +type yamlDocument struct { + ApiVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Metadata struct { + Name string `yaml:"name"` + Labels map[string]string `yaml:"labels,omitempty"` + } `yaml:"metadata"` + Spec struct { + DisplayName string `yaml:"display_name"` + CatalogItemID string `yaml:"catalog_item_id"` + UserValues []DesiredUserValue `yaml:"user_values"` + } `yaml:"spec"` +} + +// ParseError contains a parse error and the file that caused it. +type ParseError struct { + File string + Err error +} + +func (e *ParseError) Error() string { + return fmt.Sprintf("%s: %s", e.File, e.Err.Error()) +} + +// ParseResult contains the successfully parsed instances and any errors. +type ParseResult struct { + Instances []DesiredInstance + Errors []ParseError +} + +// ParseCatalogItemInstances reads YAML files directly at the given path (non-recursive) +// and parses them as CatalogItemInstance definitions. +// Files are processed in alphabetical order; for duplicate metadata.name, +// the last file alphabetically wins. +func ParseCatalogItemInstances(dir, specPath string) ParseResult { + result := ParseResult{} + + fullPath := filepath.Join(dir, specPath) + + entries, err := os.ReadDir(fullPath) + if err != nil { + result.Errors = append(result.Errors, ParseError{File: fullPath, Err: err}) + return result + } + + // Collect yaml files and sort alphabetically + var yamlFiles []string + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + ext := strings.ToLower(filepath.Ext(name)) + if ext == ".yaml" || ext == ".yml" { + yamlFiles = append(yamlFiles, name) + } + } + sort.Strings(yamlFiles) + + // Parse files; last-wins for duplicate names + seen := make(map[string]int) // name -> index in result.Instances + for _, filename := range yamlFiles { + filePath := filepath.Join(fullPath, filename) + data, err := os.ReadFile(filePath) + if err != nil { + result.Errors = append(result.Errors, ParseError{File: filename, Err: err}) + continue + } + + var doc yamlDocument + if err := yaml.Unmarshal(data, &doc); err != nil { + result.Errors = append(result.Errors, ParseError{File: filename, Err: err}) + continue + } + + if doc.Kind != "CatalogItemInstance" { + result.Errors = append(result.Errors, ParseError{ + File: filename, + Err: fmt.Errorf("unexpected kind %q, expected CatalogItemInstance", doc.Kind), + }) + continue + } + + if doc.Metadata.Name == "" { + result.Errors = append(result.Errors, ParseError{ + File: filename, + Err: fmt.Errorf("metadata.name is required"), + }) + continue + } + + if doc.Spec.CatalogItemID == "" { + result.Errors = append(result.Errors, ParseError{ + File: filename, + Err: fmt.Errorf("spec.catalog_item_id is required"), + }) + continue + } + + displayName := doc.Spec.DisplayName + if displayName == "" { + displayName = doc.Metadata.Name + } + + instance := DesiredInstance{ + Name: doc.Metadata.Name, + ApiVersion: doc.ApiVersion, + DisplayName: displayName, + CatalogItemID: doc.Spec.CatalogItemID, + UserValues: doc.Spec.UserValues, + SourceFile: filepath.Join(specPath, filename), + Labels: doc.Metadata.Labels, + } + + if idx, exists := seen[doc.Metadata.Name]; exists { + // Last file alphabetically wins + result.Instances[idx] = instance + } else { + seen[doc.Metadata.Name] = len(result.Instances) + result.Instances = append(result.Instances, instance) + } + } + + return result +} diff --git a/internal/gitops/controller/parser_test.go b/internal/gitops/controller/parser_test.go new file mode 100644 index 0000000..481c756 --- /dev/null +++ b/internal/gitops/controller/parser_test.go @@ -0,0 +1,249 @@ +package controller_test + +import ( + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/dcm-project/control-plane/internal/gitops/controller" +) + +var _ = Describe("ParseCatalogItemInstances", func() { + var tmpDir string + + BeforeEach(func() { + tmpDir = GinkgoT().TempDir() + }) + + writeFile := func(name, content string) { + err := os.MkdirAll(filepath.Join(tmpDir, "apps"), 0o755) + Expect(err).NotTo(HaveOccurred()) + err = os.WriteFile(filepath.Join(tmpDir, "apps", name), []byte(content), 0o644) + Expect(err).NotTo(HaveOccurred()) + } + + It("parses a valid CatalogItemInstance YAML", func() { + writeFile("webserver.yaml", ` +apiVersion: v1alpha1 +kind: CatalogItemInstance +metadata: + name: my-webserver +spec: + display_name: My Webserver + catalog_item_id: small-vm + user_values: + - resource: app + path: vcpu.count + value: 4 +`) + result := controller.ParseCatalogItemInstances(tmpDir, "apps") + Expect(result.Errors).To(BeEmpty()) + Expect(result.Instances).To(HaveLen(1)) + Expect(result.Instances[0].Name).To(Equal("my-webserver")) + Expect(result.Instances[0].DisplayName).To(Equal("My Webserver")) + Expect(result.Instances[0].CatalogItemID).To(Equal("small-vm")) + Expect(result.Instances[0].UserValues).To(HaveLen(1)) + Expect(result.Instances[0].UserValues[0].Path).To(Equal("vcpu.count")) + Expect(result.Instances[0].SourceFile).To(Equal("apps/webserver.yaml")) + }) + + It("parses multiple YAML files in alphabetical order", func() { + writeFile("beta.yaml", ` +apiVersion: v1alpha1 +kind: CatalogItemInstance +metadata: + name: beta-app +spec: + catalog_item_id: small-vm + user_values: [] +`) + writeFile("alpha.yaml", ` +apiVersion: v1alpha1 +kind: CatalogItemInstance +metadata: + name: alpha-app +spec: + catalog_item_id: small-vm + user_values: [] +`) + result := controller.ParseCatalogItemInstances(tmpDir, "apps") + Expect(result.Errors).To(BeEmpty()) + Expect(result.Instances).To(HaveLen(2)) + Expect(result.Instances[0].Name).To(Equal("alpha-app")) + Expect(result.Instances[1].Name).To(Equal("beta-app")) + }) + + It("last file alphabetically wins for duplicate names", func() { + writeFile("alpha.yaml", ` +apiVersion: v1alpha1 +kind: CatalogItemInstance +metadata: + name: same-name +spec: + catalog_item_id: old-item + user_values: [] +`) + writeFile("zebra.yaml", ` +apiVersion: v1alpha1 +kind: CatalogItemInstance +metadata: + name: same-name +spec: + catalog_item_id: new-item + user_values: [] +`) + result := controller.ParseCatalogItemInstances(tmpDir, "apps") + Expect(result.Errors).To(BeEmpty()) + Expect(result.Instances).To(HaveLen(1)) + Expect(result.Instances[0].CatalogItemID).To(Equal("new-item")) + }) + + It("skips invalid YAML and reports errors", func() { + writeFile("bad.yaml", `this is not valid yaml: [`) + writeFile("good.yaml", ` +apiVersion: v1alpha1 +kind: CatalogItemInstance +metadata: + name: good-app +spec: + catalog_item_id: small-vm + user_values: [] +`) + result := controller.ParseCatalogItemInstances(tmpDir, "apps") + Expect(result.Errors).To(HaveLen(1)) + Expect(result.Errors[0].File).To(Equal("bad.yaml")) + Expect(result.Instances).To(HaveLen(1)) + Expect(result.Instances[0].Name).To(Equal("good-app")) + }) + + It("skips files with wrong kind", func() { + writeFile("policy.yaml", ` +apiVersion: v1alpha1 +kind: Policy +metadata: + name: my-policy +spec: + catalog_item_id: something + user_values: [] +`) + result := controller.ParseCatalogItemInstances(tmpDir, "apps") + Expect(result.Errors).To(HaveLen(1)) + Expect(result.Errors[0].File).To(Equal("policy.yaml")) + Expect(result.Instances).To(BeEmpty()) + }) + + It("skips files missing metadata.name", func() { + writeFile("noname.yaml", ` +apiVersion: v1alpha1 +kind: CatalogItemInstance +metadata: + name: "" +spec: + catalog_item_id: small-vm + user_values: [] +`) + result := controller.ParseCatalogItemInstances(tmpDir, "apps") + Expect(result.Errors).To(HaveLen(1)) + Expect(result.Instances).To(BeEmpty()) + }) + + It("skips files missing spec.catalog_item_id", func() { + writeFile("noid.yaml", ` +apiVersion: v1alpha1 +kind: CatalogItemInstance +metadata: + name: my-app +spec: + user_values: [] +`) + result := controller.ParseCatalogItemInstances(tmpDir, "apps") + Expect(result.Errors).To(HaveLen(1)) + Expect(result.Instances).To(BeEmpty()) + }) + + It("ignores non-YAML files", func() { + writeFile("readme.txt", "This is not YAML") + writeFile("app.yaml", ` +apiVersion: v1alpha1 +kind: CatalogItemInstance +metadata: + name: my-app +spec: + catalog_item_id: small-vm + user_values: [] +`) + result := controller.ParseCatalogItemInstances(tmpDir, "apps") + Expect(result.Errors).To(BeEmpty()) + Expect(result.Instances).To(HaveLen(1)) + }) + + It("ignores subdirectories", func() { + // Create a subdirectory with a YAML file + subdir := filepath.Join(tmpDir, "apps", "subdir") + err := os.MkdirAll(subdir, 0o755) + Expect(err).NotTo(HaveOccurred()) + err = os.WriteFile(filepath.Join(subdir, "nested.yaml"), []byte(` +apiVersion: v1alpha1 +kind: CatalogItemInstance +metadata: + name: nested-app +spec: + catalog_item_id: small-vm + user_values: [] +`), 0o644) + Expect(err).NotTo(HaveOccurred()) + + writeFile("root.yaml", ` +apiVersion: v1alpha1 +kind: CatalogItemInstance +metadata: + name: root-app +spec: + catalog_item_id: small-vm + user_values: [] +`) + result := controller.ParseCatalogItemInstances(tmpDir, "apps") + Expect(result.Errors).To(BeEmpty()) + Expect(result.Instances).To(HaveLen(1)) + Expect(result.Instances[0].Name).To(Equal("root-app")) + }) + + It("supports .yml extension", func() { + writeFile("app.yml", ` +apiVersion: v1alpha1 +kind: CatalogItemInstance +metadata: + name: yml-app +spec: + catalog_item_id: small-vm + user_values: [] +`) + result := controller.ParseCatalogItemInstances(tmpDir, "apps") + Expect(result.Errors).To(BeEmpty()) + Expect(result.Instances).To(HaveLen(1)) + Expect(result.Instances[0].Name).To(Equal("yml-app")) + }) + + It("uses metadata.name as display_name when display_name is empty", func() { + writeFile("app.yaml", ` +apiVersion: v1alpha1 +kind: CatalogItemInstance +metadata: + name: my-app +spec: + catalog_item_id: small-vm + user_values: [] +`) + result := controller.ParseCatalogItemInstances(tmpDir, "apps") + Expect(result.Errors).To(BeEmpty()) + Expect(result.Instances[0].DisplayName).To(Equal("my-app")) + }) + + It("returns error when path does not exist", func() { + result := controller.ParseCatalogItemInstances(tmpDir, "nonexistent") + Expect(result.Errors).To(HaveLen(1)) + Expect(result.Instances).To(BeEmpty()) + }) +}) diff --git a/internal/gitops/controller/reconciler.go b/internal/gitops/controller/reconciler.go new file mode 100644 index 0000000..35b64e7 --- /dev/null +++ b/internal/gitops/controller/reconciler.go @@ -0,0 +1,189 @@ +package controller + +import ( + "context" + "fmt" + "log/slog" + + catalogv1alpha1 "github.com/dcm-project/control-plane/api/catalog/v1alpha1" + catalogservice "github.com/dcm-project/control-plane/internal/catalog/service" + "github.com/dcm-project/control-plane/internal/gitops/store" + "github.com/dcm-project/control-plane/internal/gitops/store/model" +) + +const ( + labelManagedBy = "gitops.dcm.io/managed-by" + labelRepository = "gitops.dcm.io/repository" + labelSourcePath = "gitops.dcm.io/source-path" + labelCommit = "gitops.dcm.io/commit" +) + +// Reconciler handles the reconciliation of a single GitRepository. +type Reconciler struct { + gitopsStore store.Store + catalogSvc catalogservice.CatalogItemInstanceService + gitClient GitOperations +} + +// NewReconciler creates a new Reconciler. +func NewReconciler(gitopsStore store.Store, catalogSvc catalogservice.CatalogItemInstanceService, gitClient GitOperations) *Reconciler { + return &Reconciler{ + gitopsStore: gitopsStore, + catalogSvc: catalogSvc, + gitClient: gitClient, + } +} + +// Reconcile performs a single reconciliation cycle for the given GitRepository. +func (r *Reconciler) Reconcile(ctx context.Context, repo model.GitRepository) error { + slog.InfoContext(ctx, "Reconciling git repository", "id", repo.ID, "url", repo.URL) + + // 1. Clone or fetch + latestCommit, err := r.gitClient.CloneOrFetch(ctx, repo.URL, repo.Branch, repo.ID) + if err != nil { + r.setError(ctx, repo.ID, fmt.Sprintf("git fetch failed: %s", err.Error())) + return fmt.Errorf("git fetch: %w", err) + } + + // 2. Check if commit changed + if latestCommit == repo.LastSyncedCommit { + slog.DebugContext(ctx, "No new commits", "id", repo.ID, "commit", latestCommit) + r.setSynced(ctx, repo.ID, latestCommit) + return nil + } + + // 3. Parse YAML files at spec.path + workDir := r.gitClient.WorkDir(repo.ID) + parseResult := ParseCatalogItemInstances(workDir, repo.Path) + + for _, pe := range parseResult.Errors { + slog.WarnContext(ctx, "Parse error", "id", repo.ID, "file", pe.File, "error", pe.Err) + } + + // 4. Get existing git-managed instances for this repo + existingInstances, err := r.listManagedInstances(ctx, repo.ID) + if err != nil { + r.setError(ctx, repo.ID, fmt.Sprintf("failed to list managed instances: %s", err.Error())) + return fmt.Errorf("list managed instances: %w", err) + } + + // 5. Classify: create / delete + desiredByName := make(map[string]DesiredInstance, len(parseResult.Instances)) + for _, d := range parseResult.Instances { + desiredByName[d.Name] = d + } + + existingByName := make(map[string]string, len(existingInstances)) // name -> id + for _, inst := range existingInstances { + existingByName[inst.DisplayName] = *inst.Uid + } + + var toCreate []DesiredInstance + for name, desired := range desiredByName { + if _, exists := existingByName[name]; !exists { + toCreate = append(toCreate, desired) + } + } + + var toDelete []string // IDs to delete + for name, id := range existingByName { + if _, exists := desiredByName[name]; !exists { + toDelete = append(toDelete, id) + slog.InfoContext(ctx, "Will delete instance removed from Git", "id", repo.ID, "instance_name", name, "instance_id", id) + } + } + + if len(toCreate) == 0 && len(toDelete) == 0 { + slog.InfoContext(ctx, "No lifecycle changes detected", "id", repo.ID) + r.setSynced(ctx, repo.ID, latestCommit) + return nil + } + + // 6. Set status IN_PROGRESS + _ = r.gitopsStore.GitRepository().UpdateSyncStatus(ctx, repo.ID, "IN_PROGRESS", "Applying lifecycle changes", repo.LastSyncedCommit) + + // 7. Apply creates + var reconcileErrors []string + for _, desired := range toCreate { + slog.InfoContext(ctx, "Creating instance from Git", "id", repo.ID, "instance_name", desired.Name) + if err := r.createInstance(ctx, repo.ID, latestCommit, desired); err != nil { + slog.ErrorContext(ctx, "Failed to create instance", "id", repo.ID, "instance_name", desired.Name, "error", err) + reconcileErrors = append(reconcileErrors, fmt.Sprintf("create %s: %s", desired.Name, err.Error())) + } + } + + // 8. Apply deletes + for _, instanceID := range toDelete { + slog.InfoContext(ctx, "Deleting instance removed from Git", "id", repo.ID, "instance_id", instanceID) + if err := r.catalogSvc.Delete(ctx, instanceID); err != nil { + slog.ErrorContext(ctx, "Failed to delete instance", "id", repo.ID, "instance_id", instanceID, "error", err) + reconcileErrors = append(reconcileErrors, fmt.Sprintf("delete %s: %s", instanceID, err.Error())) + } + } + + // 9. Update status + if len(reconcileErrors) > 0 { + r.setError(ctx, repo.ID, fmt.Sprintf("reconciliation errors: %v", reconcileErrors)) + return fmt.Errorf("reconciliation had %d errors", len(reconcileErrors)) + } + + r.setSynced(ctx, repo.ID, latestCommit) + slog.InfoContext(ctx, "Reconciliation complete", "id", repo.ID, "commit", latestCommit, + "created", len(toCreate), "deleted", len(toDelete)) + return nil +} + +func (r *Reconciler) createInstance(ctx context.Context, repoID, commit string, desired DesiredInstance) error { + apiVersion := desired.ApiVersion + if apiVersion == "" { + apiVersion = "v1alpha1" + } + + userValues := make([]catalogv1alpha1.UserValue, len(desired.UserValues)) + for i, uv := range desired.UserValues { + userValues[i] = catalogv1alpha1.UserValue{ + Resource: uv.Resource, + Path: uv.Path, + Value: uv.Value, + } + } + + req := &catalogservice.CreateCatalogItemInstanceRequest{ + ApiVersion: apiVersion, + DisplayName: desired.DisplayName, + Spec: catalogv1alpha1.CatalogItemInstanceSpec{ + CatalogItemId: desired.CatalogItemID, + UserValues: userValues, + }, + } + // Use the desired instance name as the ID + req.ID = &desired.Name + + _, err := r.catalogSvc.Create(ctx, req) + return err +} + +func (r *Reconciler) listManagedInstances(ctx context.Context, repoID string) ([]catalogv1alpha1.CatalogItemInstance, error) { + // List all instances and filter by gitops labels. + // A future optimization can add label-based filtering to the store. + result, err := r.catalogSvc.List(ctx, catalogservice.CatalogItemInstanceListOptions{}) + if err != nil { + return nil, err + } + + // TODO: filter by gitops labels once CatalogItemInstance supports labels. + // For now, return all instances — the reconciler will match by name. + return result.CatalogItemInstances, nil +} + +func (r *Reconciler) setSynced(ctx context.Context, repoID, commit string) { + if err := r.gitopsStore.GitRepository().UpdateSyncStatus(ctx, repoID, "SYNCED", "", commit); err != nil { + slog.ErrorContext(ctx, "Failed to update sync status to SYNCED", "id", repoID, "error", err) + } +} + +func (r *Reconciler) setError(ctx context.Context, repoID, message string) { + if err := r.gitopsStore.GitRepository().UpdateSyncStatus(ctx, repoID, "ERROR", message, ""); err != nil { + slog.ErrorContext(ctx, "Failed to update sync status to ERROR", "id", repoID, "error", err) + } +} diff --git a/internal/gitops/controller/repository.go b/internal/gitops/controller/repository.go new file mode 100644 index 0000000..67ba941 --- /dev/null +++ b/internal/gitops/controller/repository.go @@ -0,0 +1,78 @@ +package controller + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// GitOperations abstracts git operations for testability. +type GitOperations interface { + // CloneOrFetch clones the repo if not present, or fetches and resets if it exists. + // Returns the latest commit SHA on the specified branch. + CloneOrFetch(ctx context.Context, url, branch, repoID string) (latestCommit string, err error) + + // WorkDir returns the path to the cloned repo for a given repoID. + WorkDir(repoID string) string +} + +// GitClient implements GitOperations using the git CLI. +type GitClient struct { + baseDir string +} + +// NewGitClient creates a GitClient that stores cloned repos under baseDir. +func NewGitClient(baseDir string) *GitClient { + return &GitClient{baseDir: baseDir} +} + +func (g *GitClient) WorkDir(repoID string) string { + return filepath.Join(g.baseDir, repoID) +} + +func (g *GitClient) CloneOrFetch(ctx context.Context, url, branch, repoID string) (string, error) { + dir := g.WorkDir(repoID) + + if _, err := os.Stat(filepath.Join(dir, ".git")); os.IsNotExist(err) { + if err := os.MkdirAll(filepath.Dir(dir), 0o755); err != nil { + return "", fmt.Errorf("create parent dir: %w", err) + } + if err := g.run(ctx, g.baseDir, "git", "clone", "--branch", branch, "--single-branch", "--depth", "1", url, repoID); err != nil { + return "", fmt.Errorf("git clone: %w", err) + } + } else { + if err := g.run(ctx, dir, "git", "fetch", "origin", branch); err != nil { + return "", fmt.Errorf("git fetch: %w", err) + } + if err := g.run(ctx, dir, "git", "reset", "--hard", "origin/"+branch); err != nil { + return "", fmt.Errorf("git reset: %w", err) + } + } + + commit, err := g.output(ctx, dir, "git", "rev-parse", "HEAD") + if err != nil { + return "", fmt.Errorf("git rev-parse: %w", err) + } + return strings.TrimSpace(commit), nil +} + +func (g *GitClient) run(ctx context.Context, dir string, name string, args ...string) error { + cmd := exec.CommandContext(ctx, name, args...) + cmd.Dir = dir + cmd.Stdout = os.Stderr // log git output to stderr + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func (g *GitClient) output(ctx context.Context, dir string, name string, args ...string) (string, error) { + cmd := exec.CommandContext(ctx, name, args...) + cmd.Dir = dir + out, err := cmd.Output() + if err != nil { + return "", err + } + return string(out), nil +} diff --git a/internal/gitops/controller/suite_test.go b/internal/gitops/controller/suite_test.go new file mode 100644 index 0000000..c6da3d2 --- /dev/null +++ b/internal/gitops/controller/suite_test.go @@ -0,0 +1,13 @@ +package controller_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestController(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "GitOps Controller Suite") +} From ad20e0646f9450c7ad843a29b123ea698cb878b1 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Thu, 9 Jul 2026 15:33:00 +0200 Subject: [PATCH 06/14] (gitops): Add dcm-gitops binary and Containerfile Add the separate dcm-gitops process entry point that runs the GitOps controller. Wires up database, catalog service stack, and controller. Includes Containerfile with git-core installed for clone/fetch ops. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Ondra Machacek --- Containerfile.gitops | 34 +++++++ cmd/dcm-gitops/main.go | 11 +++ internal/gitops/app/config.go | 35 +++++++ internal/gitops/app/run.go | 172 ++++++++++++++++++++++++++++++++++ 4 files changed, 252 insertions(+) create mode 100644 Containerfile.gitops create mode 100644 cmd/dcm-gitops/main.go create mode 100644 internal/gitops/app/config.go create mode 100644 internal/gitops/app/run.go diff --git a/Containerfile.gitops b/Containerfile.gitops new file mode 100644 index 0000000..2223abb --- /dev/null +++ b/Containerfile.gitops @@ -0,0 +1,34 @@ +# Build stage +FROM registry.access.redhat.com/ubi9/go-toolset:1.25.5 AS builder + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +USER root +RUN CGO_ENABLED=0 GOOS=linux go build -buildvcs=false -o dcm-gitops ./cmd/dcm-gitops + +# Runtime stage +FROM registry.access.redhat.com/ubi9/ubi-minimal:latest + +# Install git (needed for clone/fetch operations) +RUN microdnf install -y git-core && microdnf clean all + +WORKDIR /app + +COPY --from=builder /app/dcm-gitops . + +# Create work directory for git clones +RUN mkdir -p /data/gitops + +# OpenShift runs arbitrary UIDs in group 0; g+rwX keeps dirs usable. +RUN chown -R 1001:0 /app /data/gitops && chmod -R g+rwX /app /data/gitops + +USER 1001 + +ENV GIT_WORK_DIR=/data/gitops + +ENTRYPOINT ["./dcm-gitops"] diff --git a/cmd/dcm-gitops/main.go b/cmd/dcm-gitops/main.go new file mode 100644 index 0000000..adc8d62 --- /dev/null +++ b/cmd/dcm-gitops/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "os" + + gitopsapp "github.com/dcm-project/control-plane/internal/gitops/app" +) + +func main() { + os.Exit(gitopsapp.Run()) +} diff --git a/internal/gitops/app/config.go b/internal/gitops/app/config.go new file mode 100644 index 0000000..13193c3 --- /dev/null +++ b/internal/gitops/app/config.go @@ -0,0 +1,35 @@ +package app + +import ( + "fmt" + + "github.com/kelseyhightower/envconfig" +) + +// Config holds configuration for the dcm-gitops process. +type Config struct { + Database DatabaseConfig + LogLevel string `envconfig:"LOG_LEVEL" default:"info"` + GitWorkDir string `envconfig:"GIT_WORK_DIR" default:"/tmp/dcm-gitops"` + // PollInterval is how often (in seconds) the controller reloads the list of repos from the DB. + PollInterval int `envconfig:"POLL_INTERVAL" default:"15"` +} + +// DatabaseConfig holds database connection settings. +type DatabaseConfig struct { + Type string `envconfig:"DB_TYPE" default:"pgsql"` + Hostname string `envconfig:"DB_HOST" default:"localhost"` + Port string `envconfig:"DB_PORT" default:"5432"` + Name string `envconfig:"DB_NAME" default:"control-plane"` + User string `envconfig:"DB_USER"` + Password string `envconfig:"DB_PASSWORD"` +} + +// LoadConfig reads configuration from environment variables. +func LoadConfig() (*Config, error) { + cfg := &Config{} + if err := envconfig.Process("", cfg); err != nil { + return nil, fmt.Errorf("load config: %w", err) + } + return cfg, nil +} diff --git a/internal/gitops/app/run.go b/internal/gitops/app/run.go new file mode 100644 index 0000000..6bc2b70 --- /dev/null +++ b/internal/gitops/app/run.go @@ -0,0 +1,172 @@ +package app + +import ( + "context" + "fmt" + "log/slog" + "os" + "os/signal" + "strings" + "syscall" + "time" + + catalogconfig "github.com/dcm-project/control-plane/internal/catalog/config" + catalogplacement "github.com/dcm-project/control-plane/internal/catalog/placement" + catalogservice "github.com/dcm-project/control-plane/internal/catalog/service" + catalogstore "github.com/dcm-project/control-plane/internal/catalog/store" + "github.com/dcm-project/control-plane/internal/gitops/controller" + gitopsstore "github.com/dcm-project/control-plane/internal/gitops/store" + placementpolicy "github.com/dcm-project/control-plane/internal/placement/policy" + placementservice "github.com/dcm-project/control-plane/internal/placement/service" + placementsprm "github.com/dcm-project/control-plane/internal/placement/sprm" + placementstore "github.com/dcm-project/control-plane/internal/placement/store" + policyopa "github.com/dcm-project/control-plane/internal/policy/opa" + policyservice "github.com/dcm-project/control-plane/internal/policy/service" + policystore "github.com/dcm-project/control-plane/internal/policy/store" + spstore "github.com/dcm-project/control-plane/internal/sp/store" + sprmsvc "github.com/dcm-project/control-plane/internal/sp/service/resource_manager" + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +// Run starts the dcm-gitops controller process. +func Run() int { + cfg, err := LoadConfig() + if err != nil { + slog.Error("Failed to load configuration", "error", err) + return 1 + } + + slogLogger := initLogger(cfg.LogLevel) + slog.SetDefault(slogLogger) + + slog.Info("dcm-gitops starting", + "db_type", cfg.Database.Type, + "db_host", cfg.Database.Hostname, + "db_name", cfg.Database.Name, + "git_work_dir", cfg.GitWorkDir, + "poll_interval", cfg.PollInterval, + ) + + db, err := openDB(cfg) + if err != nil { + slog.Error("Failed to initialize database", "error", err) + return 1 + } + defer func() { + if sqlDB, err := db.DB(); err == nil { + _ = sqlDB.Close() + } + }() + + // Create stores + gitopsDataStore := gitopsstore.NewStore(db) + catalogDataStore := catalogstore.NewStore(db, slogLogger) + policyDataStore := policystore.NewStore(db) + placementDataStore := placementstore.NewStore(db) + spDataStore := spstore.NewStore(db) + + // Build catalog service (same wiring as dcm-server) + opaEngine := policyopa.NewEngine() + policySvc := policyservice.NewPolicyService(policyDataStore, opaEngine) + evaluationSvc := policyservice.NewEvaluationService(policyDataStore.Policy(), opaEngine) + if err := policySvc.CompileAll(context.Background()); err != nil { + slog.Error("Failed to compile policies on startup", "error", err) + return 1 + } + + spInstanceSvc := sprmsvc.NewInstanceService(spDataStore, nil, nil) + policyClient := placementpolicy.NewServiceClient(evaluationSvc) + sprmClient := placementsprm.NewServiceClient(spInstanceSvc) + placementSvc := placementservice.NewPlacementService(placementDataStore, policyClient, sprmClient) + + pmClient := catalogplacement.NewLocalClient(placementSvc, slogLogger) + + catalogSvc, err := catalogservice.NewService(catalogDataStore, pmClient, catalogconfig.SeedConfig{}, slogLogger) + if err != nil { + slog.Error("Failed to create catalog service", "error", err) + return 1 + } + + // Create git client and reconciler + gitClient := controller.NewGitClient(cfg.GitWorkDir) + reconciler := controller.NewReconciler(gitopsDataStore, catalogSvc.CatalogItemInstance(), gitClient) + + // Create and start controller + pollInterval := time.Duration(cfg.PollInterval) * time.Second + ctrl := controller.NewController(reconciler, gitopsDataStore, pollInterval) + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + + ctrl.Start(ctx) + defer ctrl.Stop() + + slog.Info("dcm-gitops controller started") + + <-ctx.Done() + slog.Info("dcm-gitops shutting down") + return 0 +} + +func openDB(cfg *Config) (*gorm.DB, error) { + var dialector gorm.Dialector + switch cfg.Database.Type { + case "pgsql": + dsn := fmt.Sprintf("host=%s user=%s password=%s port=%s dbname=%s", + cfg.Database.Hostname, + cfg.Database.User, + cfg.Database.Password, + cfg.Database.Port, + cfg.Database.Name, + ) + dialector = postgres.Open(dsn) + case "sqlite": + dialector = sqlite.Open(cfg.Database.Name) + default: + return nil, fmt.Errorf("unsupported DB_TYPE %q", cfg.Database.Type) + } + + db, err := gorm.Open(dialector, &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + TranslateError: true, + }) + if err != nil { + return nil, fmt.Errorf("connect database: %w", err) + } + + if cfg.Database.Type == "sqlite" { + if err := db.Exec("PRAGMA foreign_keys = ON").Error; err != nil { + return nil, fmt.Errorf("enable sqlite foreign keys: %w", err) + } + } + + sqlDB, err := db.DB() + if err != nil { + return nil, fmt.Errorf("underlying db handle: %w", err) + } + sqlDB.SetMaxIdleConns(10) + sqlDB.SetMaxOpenConns(100) + sqlDB.SetConnMaxLifetime(time.Hour) + + return db, nil +} + +func initLogger(level string) *slog.Logger { + var logLevel slog.Level + switch strings.ToLower(level) { + case "debug": + logLevel = slog.LevelDebug + case "info": + logLevel = slog.LevelInfo + case "warn", "warning": + logLevel = slog.LevelWarn + case "error": + logLevel = slog.LevelError + default: + logLevel = slog.LevelInfo + } + return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel})) +} From 1a7382b64487c2a202a81947d711a8c2ca6f822a Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Thu, 16 Jul 2026 13:08:22 +0200 Subject: [PATCH 07/14] Remove redundant /git-repositories/{gitRepositoryId}/status endpoint Status is already returned as part of GET /git-repositories/{gitRepositoryId}, making the dedicated status endpoint unnecessary. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Ondra Machacek --- api/gitops/v1alpha1/openapi.yaml | 29 ---- api/gitops/v1alpha1/spec.gen.go | 120 +++++++-------- internal/gitops/api/server/server.gen.go | 147 ------------------ internal/gitops/handlers/v1alpha1/errors.go | 15 -- internal/gitops/handlers/v1alpha1/handler.go | 8 - internal/gitops/service/service.go | 10 -- pkg/gitops/client/client.gen.go | 149 ------------------- 7 files changed, 60 insertions(+), 418 deletions(-) diff --git a/api/gitops/v1alpha1/openapi.yaml b/api/gitops/v1alpha1/openapi.yaml index 36b88e2..dbecaf4 100644 --- a/api/gitops/v1alpha1/openapi.yaml +++ b/api/gitops/v1alpha1/openapi.yaml @@ -221,35 +221,6 @@ paths: '500': $ref: '#/components/responses/InternalServerError' - /git-repositories/{gitRepositoryId}/status: - get: - operationId: getGitRepositoryStatus - summary: Get git repository sync status - description: | - Retrieves the current sync status for a git repository. - parameters: - - $ref: '#/components/parameters/GitRepositoryIdPath' - - responses: - '200': - description: Sync status retrieved - content: - application/json: - schema: - $ref: '#/components/schemas/GitRepositoryStatus' - - '401': - $ref: '#/components/responses/Unauthorized' - - '403': - $ref: '#/components/responses/Forbidden' - - '404': - $ref: '#/components/responses/NotFound' - - '500': - $ref: '#/components/responses/InternalServerError' - /git-repositories/{gitRepositoryId}:sync: post: operationId: syncGitRepository diff --git a/api/gitops/v1alpha1/spec.gen.go b/api/gitops/v1alpha1/spec.gen.go index 0bba1fb..83150a3 100644 --- a/api/gitops/v1alpha1/spec.gen.go +++ b/api/gitops/v1alpha1/spec.gen.go @@ -20,66 +20,66 @@ import ( // const string: with thousands of chunks the chained `+` fold is several // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ - "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", + "//8=", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/gitops/api/server/server.gen.go b/internal/gitops/api/server/server.gen.go index 98868d0..c1a8ef9 100644 --- a/internal/gitops/api/server/server.gen.go +++ b/internal/gitops/api/server/server.gen.go @@ -33,9 +33,6 @@ type ServerInterface interface { // Update a git repository // (PUT /git-repositories/{gitRepositoryId}) UpdateGitRepository(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) - // Get git repository sync status - // (GET /git-repositories/{gitRepositoryId}/status) - GetGitRepositoryStatus(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) // Trigger immediate sync // (POST /git-repositories/{gitRepositoryId}:sync) SyncGitRepository(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) @@ -75,12 +72,6 @@ func (_ Unimplemented) UpdateGitRepository(w http.ResponseWriter, r *http.Reques w.WriteHeader(http.StatusNotImplemented) } -// Get git repository sync status -// (GET /git-repositories/{gitRepositoryId}/status) -func (_ Unimplemented) GetGitRepositoryStatus(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) { - w.WriteHeader(http.StatusNotImplemented) -} - // Trigger immediate sync // (POST /git-repositories/{gitRepositoryId}:sync) func (_ Unimplemented) SyncGitRepository(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) { @@ -253,32 +244,6 @@ func (siw *ServerInterfaceWrapper) UpdateGitRepository(w http.ResponseWriter, r handler.ServeHTTP(w, r) } -// GetGitRepositoryStatus operation middleware -func (siw *ServerInterfaceWrapper) GetGitRepositoryStatus(w http.ResponseWriter, r *http.Request) { - - var err error - _ = err - - // ------------- Path parameter "gitRepositoryId" ------------- - var gitRepositoryId GitRepositoryIdPath - - err = runtime.BindStyledParameterWithOptions("simple", "gitRepositoryId", chi.URLParam(r, "gitRepositoryId"), &gitRepositoryId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "gitRepositoryId", Err: err}) - return - } - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetGitRepositoryStatus(w, r, gitRepositoryId) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - // SyncGitRepository operation middleware func (siw *ServerInterfaceWrapper) SyncGitRepository(w http.ResponseWriter, r *http.Request) { @@ -433,9 +398,6 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Put(options.BaseURL+"/git-repositories/{gitRepositoryId}", wrapper.UpdateGitRepository) }) - r.Group(func(r chi.Router) { - r.Get(options.BaseURL+"/git-repositories/{gitRepositoryId}/status", wrapper.GetGitRepositoryStatus) - }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/git-repositories/{gitRepositoryId}:sync", wrapper.SyncGitRepository) }) @@ -879,86 +841,6 @@ func (response UpdateGitRepository500JSONResponse) VisitUpdateGitRepositoryRespo return err } -type GetGitRepositoryStatusRequestObject struct { - GitRepositoryId GitRepositoryIdPath `json:"gitRepositoryId"` -} - -type GetGitRepositoryStatusResponseObject interface { - VisitGetGitRepositoryStatusResponse(w http.ResponseWriter) error -} - -type GetGitRepositoryStatus200JSONResponse GitRepositoryStatus - -func (response GetGitRepositoryStatus200JSONResponse) VisitGetGitRepositoryStatusResponse(w http.ResponseWriter) error { - - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(response); err != nil { - return err - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - _, err := buf.WriteTo(w) - return err -} - -type GetGitRepositoryStatus401JSONResponse struct{ UnauthorizedJSONResponse } - -func (response GetGitRepositoryStatus401JSONResponse) VisitGetGitRepositoryStatusResponse(w http.ResponseWriter) error { - - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(response); err != nil { - return err - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(401) - _, err := buf.WriteTo(w) - return err -} - -type GetGitRepositoryStatus403JSONResponse struct{ ForbiddenJSONResponse } - -func (response GetGitRepositoryStatus403JSONResponse) VisitGetGitRepositoryStatusResponse(w http.ResponseWriter) error { - - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(response); err != nil { - return err - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(403) - _, err := buf.WriteTo(w) - return err -} - -type GetGitRepositoryStatus404JSONResponse struct{ NotFoundJSONResponse } - -func (response GetGitRepositoryStatus404JSONResponse) VisitGetGitRepositoryStatusResponse(w http.ResponseWriter) error { - - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(response); err != nil { - return err - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(404) - _, err := buf.WriteTo(w) - return err -} - -type GetGitRepositoryStatus500JSONResponse struct { - InternalServerErrorJSONResponse -} - -func (response GetGitRepositoryStatus500JSONResponse) VisitGetGitRepositoryStatusResponse(w http.ResponseWriter) error { - - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(response); err != nil { - return err - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(500) - _, err := buf.WriteTo(w) - return err -} - type SyncGitRepositoryRequestObject struct { GitRepositoryId GitRepositoryIdPath `json:"gitRepositoryId"` } @@ -1056,9 +938,6 @@ type StrictServerInterface interface { // Update a git repository // (PUT /git-repositories/{gitRepositoryId}) UpdateGitRepository(ctx context.Context, request UpdateGitRepositoryRequestObject) (UpdateGitRepositoryResponseObject, error) - // Get git repository sync status - // (GET /git-repositories/{gitRepositoryId}/status) - GetGitRepositoryStatus(ctx context.Context, request GetGitRepositoryStatusRequestObject) (GetGitRepositoryStatusResponseObject, error) // Trigger immediate sync // (POST /git-repositories/{gitRepositoryId}:sync) SyncGitRepository(ctx context.Context, request SyncGitRepositoryRequestObject) (SyncGitRepositoryResponseObject, error) @@ -1237,32 +1116,6 @@ func (sh *strictHandler) UpdateGitRepository(w http.ResponseWriter, r *http.Requ } } -// GetGitRepositoryStatus operation middleware -func (sh *strictHandler) GetGitRepositoryStatus(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) { - var request GetGitRepositoryStatusRequestObject - - request.GitRepositoryId = gitRepositoryId - - handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { - return sh.ssi.GetGitRepositoryStatus(ctx, request.(GetGitRepositoryStatusRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetGitRepositoryStatus") - } - - response, err := handler(r.Context(), w, r, request) - - if err != nil { - sh.options.ResponseErrorHandlerFunc(w, r, err) - } else if validResponse, ok := response.(GetGitRepositoryStatusResponseObject); ok { - if err := validResponse.VisitGetGitRepositoryStatusResponse(w); err != nil { - sh.options.ResponseErrorHandlerFunc(w, r, err) - } - } else if response != nil { - sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) - } -} - // SyncGitRepository operation middleware func (sh *strictHandler) SyncGitRepository(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) { var request SyncGitRepositoryRequestObject diff --git a/internal/gitops/handlers/v1alpha1/errors.go b/internal/gitops/handlers/v1alpha1/errors.go index 0a6d4d8..bd2e20a 100644 --- a/internal/gitops/handlers/v1alpha1/errors.go +++ b/internal/gitops/handlers/v1alpha1/errors.go @@ -104,21 +104,6 @@ func handleDeleteError(err error) server.DeleteGitRepositoryResponseObject { } } -func handleGetStatusError(err error) server.GetGitRepositoryStatusResponseObject { - if errors.Is(err, service.ErrNotFound) { - return server.GetGitRepositoryStatus404JSONResponse{ - NotFoundJSONResponse: server.NotFoundJSONResponse(buildError( - 404, v1alpha1.NOTFOUND, "Resource not found", err.Error(), - )), - } - } - return server.GetGitRepositoryStatus500JSONResponse{ - InternalServerErrorJSONResponse: server.InternalServerErrorJSONResponse(buildError( - 500, v1alpha1.INTERNAL, "Internal server error", err.Error(), - )), - } -} - func handleSyncError(err error) server.SyncGitRepositoryResponseObject { if errors.Is(err, service.ErrNotFound) { return server.SyncGitRepository404JSONResponse{ diff --git a/internal/gitops/handlers/v1alpha1/handler.go b/internal/gitops/handlers/v1alpha1/handler.go index 6976be9..3981e93 100644 --- a/internal/gitops/handlers/v1alpha1/handler.go +++ b/internal/gitops/handlers/v1alpha1/handler.go @@ -79,14 +79,6 @@ func (h *Handler) DeleteGitRepository(ctx context.Context, request server.Delete return server.DeleteGitRepository204Response{}, nil } -func (h *Handler) GetGitRepositoryStatus(ctx context.Context, request server.GetGitRepositoryStatusRequestObject) (server.GetGitRepositoryStatusResponseObject, error) { - status, err := h.service.GetStatus(ctx, request.GitRepositoryId) - if err != nil { - return handleGetStatusError(err), nil - } - return server.GetGitRepositoryStatus200JSONResponse(*status), nil -} - func (h *Handler) SyncGitRepository(ctx context.Context, request server.SyncGitRepositoryRequestObject) (server.SyncGitRepositoryResponseObject, error) { status, err := h.service.TriggerSync(ctx, request.GitRepositoryId) if err != nil { diff --git a/internal/gitops/service/service.go b/internal/gitops/service/service.go index 4440287..eda1d74 100644 --- a/internal/gitops/service/service.go +++ b/internal/gitops/service/service.go @@ -23,7 +23,6 @@ type GitRepositoryService interface { Create(ctx context.Context, req v1alpha1.GitRepository, clientID *string) (*v1alpha1.GitRepository, error) Update(ctx context.Context, id string, req v1alpha1.GitRepository) (*v1alpha1.GitRepository, error) Delete(ctx context.Context, id string) error - GetStatus(ctx context.Context, id string) (*v1alpha1.GitRepositoryStatus, error) TriggerSync(ctx context.Context, id string) (*v1alpha1.GitRepositoryStatus, error) } @@ -106,15 +105,6 @@ func (s *gitRepositoryService) Delete(ctx context.Context, id string) error { return nil } -func (s *gitRepositoryService) GetStatus(ctx context.Context, id string) (*v1alpha1.GitRepositoryStatus, error) { - repo, err := s.store.GitRepository().Get(ctx, id) - if err != nil { - return nil, mapStoreError(err) - } - status := modelToStatus(repo) - return &status, nil -} - func (s *gitRepositoryService) TriggerSync(ctx context.Context, id string) (*v1alpha1.GitRepositoryStatus, error) { // Verify the repo exists repo, err := s.store.GitRepository().Get(ctx, id) diff --git a/pkg/gitops/client/client.gen.go b/pkg/gitops/client/client.gen.go index e24254b..e62664c 100644 --- a/pkg/gitops/client/client.gen.go +++ b/pkg/gitops/client/client.gen.go @@ -109,9 +109,6 @@ type ClientInterface interface { UpdateGitRepository(ctx context.Context, gitRepositoryId GitRepositoryIdPath, body UpdateGitRepositoryJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - // GetGitRepositoryStatus request - GetGitRepositoryStatus(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*http.Response, error) - // SyncGitRepository request SyncGitRepository(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*http.Response, error) } @@ -200,18 +197,6 @@ func (c *Client) UpdateGitRepository(ctx context.Context, gitRepositoryId GitRep return c.Client.Do(req) } -func (c *Client) GetGitRepositoryStatus(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetGitRepositoryStatusRequest(c.Server, gitRepositoryId) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - func (c *Client) SyncGitRepository(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewSyncGitRepositoryRequest(c.Server, gitRepositoryId) if err != nil { @@ -472,40 +457,6 @@ func NewUpdateGitRepositoryRequestWithBody(server string, gitRepositoryId GitRep return req, nil } -// NewGetGitRepositoryStatusRequest generates requests for GetGitRepositoryStatus -func NewGetGitRepositoryStatusRequest(server string, gitRepositoryId GitRepositoryIdPath) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "gitRepositoryId", gitRepositoryId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/git-repositories/%s/status", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - // NewSyncGitRepositoryRequest generates requests for SyncGitRepository func NewSyncGitRepositoryRequest(server string, gitRepositoryId GitRepositoryIdPath) (*http.Request, error) { var err error @@ -602,9 +553,6 @@ type ClientWithResponsesInterface interface { UpdateGitRepositoryWithResponse(ctx context.Context, gitRepositoryId GitRepositoryIdPath, body UpdateGitRepositoryJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateGitRepositoryResponse, error) - // GetGitRepositoryStatusWithResponse request - GetGitRepositoryStatusWithResponse(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*GetGitRepositoryStatusResponse, error) - // SyncGitRepositoryWithResponse request SyncGitRepositoryWithResponse(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*SyncGitRepositoryResponse, error) } @@ -780,40 +728,6 @@ func (r UpdateGitRepositoryResponse) ContentType() string { return "" } -type GetGitRepositoryStatusResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *GitRepositoryStatus - JSON401 *Unauthorized - JSON403 *Forbidden - JSON404 *NotFound - JSON500 *InternalServerError -} - -// Status returns HTTPResponse.Status -func (r GetGitRepositoryStatusResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r GetGitRepositoryStatusResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r GetGitRepositoryStatusResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - type SyncGitRepositoryResponse struct { Body []byte HTTPResponse *http.Response @@ -909,15 +823,6 @@ func (c *ClientWithResponses) UpdateGitRepositoryWithResponse(ctx context.Contex return ParseUpdateGitRepositoryResponse(rsp) } -// GetGitRepositoryStatusWithResponse request returning *GetGitRepositoryStatusResponse -func (c *ClientWithResponses) GetGitRepositoryStatusWithResponse(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*GetGitRepositoryStatusResponse, error) { - rsp, err := c.GetGitRepositoryStatus(ctx, gitRepositoryId, reqEditors...) - if err != nil { - return nil, err - } - return ParseGetGitRepositoryStatusResponse(rsp) -} - // SyncGitRepositoryWithResponse request returning *SyncGitRepositoryResponse func (c *ClientWithResponses) SyncGitRepositoryWithResponse(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*SyncGitRepositoryResponse, error) { rsp, err := c.SyncGitRepository(ctx, gitRepositoryId, reqEditors...) @@ -1204,60 +1109,6 @@ func ParseUpdateGitRepositoryResponse(rsp *http.Response) (*UpdateGitRepositoryR return response, nil } -// ParseGetGitRepositoryStatusResponse parses an HTTP response from a GetGitRepositoryStatusWithResponse call -func ParseGetGitRepositoryStatusResponse(rsp *http.Response) (*GetGitRepositoryStatusResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &GetGitRepositoryStatusResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest GitRepositoryStatus - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: - var dest Unauthorized - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON401 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: - var dest Forbidden - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON403 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: - var dest NotFound - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON404 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: - var dest InternalServerError - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON500 = &dest - - } - - return response, nil -} - // ParseSyncGitRepositoryResponse parses an HTTP response from a SyncGitRepositoryWithResponse call func ParseSyncGitRepositoryResponse(rsp *http.Response) (*SyncGitRepositoryResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) From 594d55ab2681afefab7bad21650382feada7867d Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Thu, 30 Jul 2026 13:45:40 +0200 Subject: [PATCH 08/14] Fix critical and high-priority issues in gitops subsystem - Add db.AutoMigrate for GitRepository in dcm-gitops startup - Remove unimplemented sync endpoint (POST :sync) from API, handlers, service, and generated server/client code - Validate repoID format in GitClient to prevent directory traversal - Add nil check on inst.Uid to prevent panic in reconciler - Abort reconciliation when all YAML files fail to parse, preventing mass-deletion of existing instances - Add 2-minute context timeout for git clone/fetch operations - Add per-repo mutex in controller to prevent concurrent reconciliations - Add input validation in service layer: display_name, URL format, interval_seconds bounds, retry policy bounds Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Ondra Machacek --- api/gitops/v1alpha1/openapi.yaml | 30 ---- api/gitops/v1alpha1/spec.gen.go | 118 ++++++++------- internal/gitops/api/server/server.gen.go | 146 ------------------ internal/gitops/app/run.go | 7 + internal/gitops/controller/controller.go | 33 ++++- internal/gitops/controller/reconciler.go | 22 ++- internal/gitops/controller/repository.go | 8 + internal/gitops/handlers/v1alpha1/errors.go | 14 -- internal/gitops/handlers/v1alpha1/handler.go | 9 -- internal/gitops/service/service.go | 54 +++++-- pkg/gitops/client/client.gen.go | 148 ------------------- 11 files changed, 162 insertions(+), 427 deletions(-) diff --git a/api/gitops/v1alpha1/openapi.yaml b/api/gitops/v1alpha1/openapi.yaml index dbecaf4..1b21796 100644 --- a/api/gitops/v1alpha1/openapi.yaml +++ b/api/gitops/v1alpha1/openapi.yaml @@ -221,36 +221,6 @@ paths: '500': $ref: '#/components/responses/InternalServerError' - /git-repositories/{gitRepositoryId}:sync: - post: - operationId: syncGitRepository - summary: Trigger immediate sync - description: | - Triggers an immediate reconciliation for a git repository, - bypassing the polling interval. - parameters: - - $ref: '#/components/parameters/GitRepositoryIdPath' - - responses: - '202': - description: Sync triggered - content: - application/json: - schema: - $ref: '#/components/schemas/GitRepositoryStatus' - - '401': - $ref: '#/components/responses/Unauthorized' - - '403': - $ref: '#/components/responses/Forbidden' - - '404': - $ref: '#/components/responses/NotFound' - - '500': - $ref: '#/components/responses/InternalServerError' - components: parameters: GitRepositoryIdPath: diff --git a/api/gitops/v1alpha1/spec.gen.go b/api/gitops/v1alpha1/spec.gen.go index 83150a3..7e8b10e 100644 --- a/api/gitops/v1alpha1/spec.gen.go +++ b/api/gitops/v1alpha1/spec.gen.go @@ -20,66 +20,64 @@ import ( // const string: with thousands of chunks the chained `+` fold is several // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ - "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", - "//8=", + "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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/gitops/api/server/server.gen.go b/internal/gitops/api/server/server.gen.go index c1a8ef9..d3c3876 100644 --- a/internal/gitops/api/server/server.gen.go +++ b/internal/gitops/api/server/server.gen.go @@ -33,9 +33,6 @@ type ServerInterface interface { // Update a git repository // (PUT /git-repositories/{gitRepositoryId}) UpdateGitRepository(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) - // Trigger immediate sync - // (POST /git-repositories/{gitRepositoryId}:sync) - SyncGitRepository(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) } // Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. @@ -72,12 +69,6 @@ func (_ Unimplemented) UpdateGitRepository(w http.ResponseWriter, r *http.Reques w.WriteHeader(http.StatusNotImplemented) } -// Trigger immediate sync -// (POST /git-repositories/{gitRepositoryId}:sync) -func (_ Unimplemented) SyncGitRepository(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) { - w.WriteHeader(http.StatusNotImplemented) -} - // ServerInterfaceWrapper converts contexts to parameters. type ServerInterfaceWrapper struct { Handler ServerInterface @@ -244,32 +235,6 @@ func (siw *ServerInterfaceWrapper) UpdateGitRepository(w http.ResponseWriter, r handler.ServeHTTP(w, r) } -// SyncGitRepository operation middleware -func (siw *ServerInterfaceWrapper) SyncGitRepository(w http.ResponseWriter, r *http.Request) { - - var err error - _ = err - - // ------------- Path parameter "gitRepositoryId" ------------- - var gitRepositoryId GitRepositoryIdPath - - err = runtime.BindStyledParameterWithOptions("simple", "gitRepositoryId", chi.URLParam(r, "gitRepositoryId"), &gitRepositoryId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "gitRepositoryId", Err: err}) - return - } - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.SyncGitRepository(w, r, gitRepositoryId) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - type UnescapedCookieParamError struct { ParamName string Err error @@ -398,9 +363,6 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Put(options.BaseURL+"/git-repositories/{gitRepositoryId}", wrapper.UpdateGitRepository) }) - r.Group(func(r chi.Router) { - r.Post(options.BaseURL+"/git-repositories/{gitRepositoryId}:sync", wrapper.SyncGitRepository) - }) return r } @@ -841,86 +803,6 @@ func (response UpdateGitRepository500JSONResponse) VisitUpdateGitRepositoryRespo return err } -type SyncGitRepositoryRequestObject struct { - GitRepositoryId GitRepositoryIdPath `json:"gitRepositoryId"` -} - -type SyncGitRepositoryResponseObject interface { - VisitSyncGitRepositoryResponse(w http.ResponseWriter) error -} - -type SyncGitRepository202JSONResponse GitRepositoryStatus - -func (response SyncGitRepository202JSONResponse) VisitSyncGitRepositoryResponse(w http.ResponseWriter) error { - - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(response); err != nil { - return err - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(202) - _, err := buf.WriteTo(w) - return err -} - -type SyncGitRepository401JSONResponse struct{ UnauthorizedJSONResponse } - -func (response SyncGitRepository401JSONResponse) VisitSyncGitRepositoryResponse(w http.ResponseWriter) error { - - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(response); err != nil { - return err - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(401) - _, err := buf.WriteTo(w) - return err -} - -type SyncGitRepository403JSONResponse struct{ ForbiddenJSONResponse } - -func (response SyncGitRepository403JSONResponse) VisitSyncGitRepositoryResponse(w http.ResponseWriter) error { - - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(response); err != nil { - return err - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(403) - _, err := buf.WriteTo(w) - return err -} - -type SyncGitRepository404JSONResponse struct{ NotFoundJSONResponse } - -func (response SyncGitRepository404JSONResponse) VisitSyncGitRepositoryResponse(w http.ResponseWriter) error { - - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(response); err != nil { - return err - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(404) - _, err := buf.WriteTo(w) - return err -} - -type SyncGitRepository500JSONResponse struct { - InternalServerErrorJSONResponse -} - -func (response SyncGitRepository500JSONResponse) VisitSyncGitRepositoryResponse(w http.ResponseWriter) error { - - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(response); err != nil { - return err - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(500) - _, err := buf.WriteTo(w) - return err -} - // StrictServerInterface represents all server handlers. type StrictServerInterface interface { // List git repositories @@ -938,9 +820,6 @@ type StrictServerInterface interface { // Update a git repository // (PUT /git-repositories/{gitRepositoryId}) UpdateGitRepository(ctx context.Context, request UpdateGitRepositoryRequestObject) (UpdateGitRepositoryResponseObject, error) - // Trigger immediate sync - // (POST /git-repositories/{gitRepositoryId}:sync) - SyncGitRepository(ctx context.Context, request SyncGitRepositoryRequestObject) (SyncGitRepositoryResponseObject, error) } type StrictHandlerFunc func(ctx context.Context, w http.ResponseWriter, r *http.Request, request any) (any, error) @@ -1116,28 +995,3 @@ func (sh *strictHandler) UpdateGitRepository(w http.ResponseWriter, r *http.Requ } } -// SyncGitRepository operation middleware -func (sh *strictHandler) SyncGitRepository(w http.ResponseWriter, r *http.Request, gitRepositoryId GitRepositoryIdPath) { - var request SyncGitRepositoryRequestObject - - request.GitRepositoryId = gitRepositoryId - - handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { - return sh.ssi.SyncGitRepository(ctx, request.(SyncGitRepositoryRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "SyncGitRepository") - } - - response, err := handler(r.Context(), w, r, request) - - if err != nil { - sh.options.ResponseErrorHandlerFunc(w, r, err) - } else if validResponse, ok := response.(SyncGitRepositoryResponseObject); ok { - if err := validResponse.VisitSyncGitRepositoryResponse(w); err != nil { - sh.options.ResponseErrorHandlerFunc(w, r, err) - } - } else if response != nil { - sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) - } -} diff --git a/internal/gitops/app/run.go b/internal/gitops/app/run.go index 6bc2b70..3011f24 100644 --- a/internal/gitops/app/run.go +++ b/internal/gitops/app/run.go @@ -16,6 +16,7 @@ import ( catalogstore "github.com/dcm-project/control-plane/internal/catalog/store" "github.com/dcm-project/control-plane/internal/gitops/controller" gitopsstore "github.com/dcm-project/control-plane/internal/gitops/store" + gitopsmodel "github.com/dcm-project/control-plane/internal/gitops/store/model" placementpolicy "github.com/dcm-project/control-plane/internal/placement/policy" placementservice "github.com/dcm-project/control-plane/internal/placement/service" placementsprm "github.com/dcm-project/control-plane/internal/placement/sprm" @@ -61,6 +62,12 @@ func Run() int { } }() + // Migrate gitops schema + if err := db.AutoMigrate(&gitopsmodel.GitRepository{}); err != nil { + slog.Error("Failed to migrate gitops schema", "error", err) + return 1 + } + // Create stores gitopsDataStore := gitopsstore.NewStore(db) catalogDataStore := catalogstore.NewStore(db, slogLogger) diff --git a/internal/gitops/controller/controller.go b/internal/gitops/controller/controller.go index 31166ad..b40e00f 100644 --- a/internal/gitops/controller/controller.go +++ b/internal/gitops/controller/controller.go @@ -12,11 +12,15 @@ import ( // Controller manages per-GitRepository reconciliation goroutines. type Controller struct { - reconciler *Reconciler - store store.Store + reconciler *Reconciler + store store.Store pollInterval time.Duration // how often to reload repos from DB - stopCh chan struct{} - wg sync.WaitGroup + stopCh chan struct{} + wg sync.WaitGroup + + // repoLocks prevents concurrent reconciliations of the same repository. + repoLocks map[string]*sync.Mutex + repoLocksMu sync.Mutex } // NewController creates a new Controller. @@ -27,6 +31,7 @@ func NewController(reconciler *Reconciler, gitopsStore store.Store, pollInterval store: gitopsStore, pollInterval: pollInterval, stopCh: make(chan struct{}), + repoLocks: make(map[string]*sync.Mutex), } } @@ -87,8 +92,28 @@ func (c *Controller) reconcileAll(ctx context.Context) { } } + mu := c.repoLock(repo.ID) + if !mu.TryLock() { + slog.DebugContext(ctx, "Skipping repo, reconciliation already in progress", "repo_id", repo.ID) + continue + } + if err := c.reconciler.Reconcile(ctx, repo); err != nil { slog.ErrorContext(ctx, "Reconciliation failed", "repo_id", repo.ID, "error", err) } + mu.Unlock() + } +} + +// repoLock returns a per-repository mutex, creating one if it doesn't exist. +func (c *Controller) repoLock(repoID string) *sync.Mutex { + c.repoLocksMu.Lock() + defer c.repoLocksMu.Unlock() + + mu, ok := c.repoLocks[repoID] + if !ok { + mu = &sync.Mutex{} + c.repoLocks[repoID] = mu } + return mu } diff --git a/internal/gitops/controller/reconciler.go b/internal/gitops/controller/reconciler.go index 35b64e7..e6425a8 100644 --- a/internal/gitops/controller/reconciler.go +++ b/internal/gitops/controller/reconciler.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "time" catalogv1alpha1 "github.com/dcm-project/control-plane/api/catalog/v1alpha1" catalogservice "github.com/dcm-project/control-plane/internal/catalog/service" @@ -34,12 +35,18 @@ func NewReconciler(gitopsStore store.Store, catalogSvc catalogservice.CatalogIte } } +// gitOperationTimeout is the maximum time allowed for git clone/fetch operations. +const gitOperationTimeout = 2 * time.Minute + // Reconcile performs a single reconciliation cycle for the given GitRepository. func (r *Reconciler) Reconcile(ctx context.Context, repo model.GitRepository) error { slog.InfoContext(ctx, "Reconciling git repository", "id", repo.ID, "url", repo.URL) - // 1. Clone or fetch - latestCommit, err := r.gitClient.CloneOrFetch(ctx, repo.URL, repo.Branch, repo.ID) + // 1. Clone or fetch (with timeout to prevent hanging on unresponsive servers) + gitCtx, gitCancel := context.WithTimeout(ctx, gitOperationTimeout) + defer gitCancel() + + latestCommit, err := r.gitClient.CloneOrFetch(gitCtx, repo.URL, repo.Branch, repo.ID) if err != nil { r.setError(ctx, repo.ID, fmt.Sprintf("git fetch failed: %s", err.Error())) return fmt.Errorf("git fetch: %w", err) @@ -60,6 +67,13 @@ func (r *Reconciler) Reconcile(ctx context.Context, repo model.GitRepository) er slog.WarnContext(ctx, "Parse error", "id", repo.ID, "file", pe.File, "error", pe.Err) } + // Abort reconciliation if all files failed to parse — this prevents + // mass-deletion of existing instances due to transient parse failures. + if len(parseResult.Instances) == 0 && len(parseResult.Errors) > 0 { + r.setError(ctx, repo.ID, fmt.Sprintf("all %d YAML files failed to parse, aborting reconciliation", len(parseResult.Errors))) + return fmt.Errorf("all %d YAML files failed to parse", len(parseResult.Errors)) + } + // 4. Get existing git-managed instances for this repo existingInstances, err := r.listManagedInstances(ctx, repo.ID) if err != nil { @@ -75,6 +89,10 @@ func (r *Reconciler) Reconcile(ctx context.Context, repo model.GitRepository) er existingByName := make(map[string]string, len(existingInstances)) // name -> id for _, inst := range existingInstances { + if inst.Uid == nil { + slog.WarnContext(ctx, "Skipping instance with nil UID", "id", repo.ID, "display_name", inst.DisplayName) + continue + } existingByName[inst.DisplayName] = *inst.Uid } diff --git a/internal/gitops/controller/repository.go b/internal/gitops/controller/repository.go index 67ba941..839960e 100644 --- a/internal/gitops/controller/repository.go +++ b/internal/gitops/controller/repository.go @@ -6,9 +6,13 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "strings" ) +// validRepoID matches DNS-1123 label format used for git repository IDs. +var validRepoID = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$`) + // GitOperations abstracts git operations for testability. type GitOperations interface { // CloneOrFetch clones the repo if not present, or fetches and resets if it exists. @@ -34,6 +38,10 @@ func (g *GitClient) WorkDir(repoID string) string { } func (g *GitClient) CloneOrFetch(ctx context.Context, url, branch, repoID string) (string, error) { + if !validRepoID.MatchString(repoID) { + return "", fmt.Errorf("invalid repo ID %q: must match DNS-1123 label format", repoID) + } + dir := g.WorkDir(repoID) if _, err := os.Stat(filepath.Join(dir, ".git")); os.IsNotExist(err) { diff --git a/internal/gitops/handlers/v1alpha1/errors.go b/internal/gitops/handlers/v1alpha1/errors.go index bd2e20a..023fe68 100644 --- a/internal/gitops/handlers/v1alpha1/errors.go +++ b/internal/gitops/handlers/v1alpha1/errors.go @@ -104,17 +104,3 @@ func handleDeleteError(err error) server.DeleteGitRepositoryResponseObject { } } -func handleSyncError(err error) server.SyncGitRepositoryResponseObject { - if errors.Is(err, service.ErrNotFound) { - return server.SyncGitRepository404JSONResponse{ - NotFoundJSONResponse: server.NotFoundJSONResponse(buildError( - 404, v1alpha1.NOTFOUND, "Resource not found", err.Error(), - )), - } - } - return server.SyncGitRepository500JSONResponse{ - InternalServerErrorJSONResponse: server.InternalServerErrorJSONResponse(buildError( - 500, v1alpha1.INTERNAL, "Internal server error", err.Error(), - )), - } -} diff --git a/internal/gitops/handlers/v1alpha1/handler.go b/internal/gitops/handlers/v1alpha1/handler.go index 3981e93..e892d4a 100644 --- a/internal/gitops/handlers/v1alpha1/handler.go +++ b/internal/gitops/handlers/v1alpha1/handler.go @@ -79,12 +79,3 @@ func (h *Handler) DeleteGitRepository(ctx context.Context, request server.Delete return server.DeleteGitRepository204Response{}, nil } -func (h *Handler) SyncGitRepository(ctx context.Context, request server.SyncGitRepositoryRequestObject) (server.SyncGitRepositoryResponseObject, error) { - status, err := h.service.TriggerSync(ctx, request.GitRepositoryId) - if err != nil { - return handleSyncError(err), nil - } - - slog.InfoContext(ctx, "GitRepository sync triggered", "id", request.GitRepositoryId) - return server.SyncGitRepository202JSONResponse(*status), nil -} diff --git a/internal/gitops/service/service.go b/internal/gitops/service/service.go index eda1d74..1fd4a71 100644 --- a/internal/gitops/service/service.go +++ b/internal/gitops/service/service.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "net/url" v1alpha1 "github.com/dcm-project/control-plane/api/gitops/v1alpha1" "github.com/dcm-project/control-plane/internal/gitops/store" @@ -11,6 +12,13 @@ import ( "github.com/google/uuid" ) +const ( + minIntervalSeconds = 10 + maxIntervalSeconds = 86400 + maxRetries = 100 + maxBackoffSeconds = 3600 +) + var ( ErrNotFound = errors.New("git repository not found") ErrAlreadyExists = errors.New("git repository already exists") @@ -23,7 +31,6 @@ type GitRepositoryService interface { Create(ctx context.Context, req v1alpha1.GitRepository, clientID *string) (*v1alpha1.GitRepository, error) Update(ctx context.Context, id string, req v1alpha1.GitRepository) (*v1alpha1.GitRepository, error) Delete(ctx context.Context, id string) error - TriggerSync(ctx context.Context, id string) (*v1alpha1.GitRepositoryStatus, error) } type gitRepositoryService struct { @@ -70,8 +77,8 @@ func (s *gitRepositoryService) Get(ctx context.Context, id string) (*v1alpha1.Gi func (s *gitRepositoryService) Create(ctx context.Context, req v1alpha1.GitRepository, clientID *string) (*v1alpha1.GitRepository, error) { id := getOrGenerateID(clientID) - if req.Spec.Url == "" { - return nil, fmt.Errorf("%w: spec.url is required", ErrInvalidArgument) + if err := validateGitRepository(req); err != nil { + return nil, err } m := apiToModel(id, req) @@ -84,8 +91,8 @@ func (s *gitRepositoryService) Create(ctx context.Context, req v1alpha1.GitRepos } func (s *gitRepositoryService) Update(ctx context.Context, id string, req v1alpha1.GitRepository) (*v1alpha1.GitRepository, error) { - if req.Spec.Url == "" { - return nil, fmt.Errorf("%w: spec.url is required", ErrInvalidArgument) + if err := validateGitRepository(req); err != nil { + return nil, err } m := apiToModel(id, req) @@ -105,16 +112,35 @@ func (s *gitRepositoryService) Delete(ctx context.Context, id string) error { return nil } -func (s *gitRepositoryService) TriggerSync(ctx context.Context, id string) (*v1alpha1.GitRepositoryStatus, error) { - // Verify the repo exists - repo, err := s.store.GitRepository().Get(ctx, id) - if err != nil { - return nil, mapStoreError(err) +func validateGitRepository(req v1alpha1.GitRepository) error { + if req.DisplayName == "" { + return fmt.Errorf("%w: display_name is required", ErrInvalidArgument) } - // The actual sync is performed by dcm-gitops; here we just return current status. - // A future enhancement can notify the controller via a channel. - status := modelToStatus(repo) - return &status, nil + if len(req.DisplayName) > 63 { + return fmt.Errorf("%w: display_name must be at most 63 characters", ErrInvalidArgument) + } + if req.Spec.Url == "" { + return fmt.Errorf("%w: spec.url is required", ErrInvalidArgument) + } + if u, err := url.Parse(req.Spec.Url); err != nil || u.Scheme == "" || u.Host == "" { + return fmt.Errorf("%w: spec.url must be a valid URL with scheme and host", ErrInvalidArgument) + } + if req.Spec.IntervalSeconds != nil { + v := int(*req.Spec.IntervalSeconds) + if v < minIntervalSeconds || v > maxIntervalSeconds { + return fmt.Errorf("%w: spec.interval_seconds must be between %d and %d", ErrInvalidArgument, minIntervalSeconds, maxIntervalSeconds) + } + } + if req.Spec.Reconciliation != nil && req.Spec.Reconciliation.RetryPolicy != nil { + rp := req.Spec.Reconciliation.RetryPolicy + if rp.MaxRetries != nil && (int(*rp.MaxRetries) < 0 || int(*rp.MaxRetries) > maxRetries) { + return fmt.Errorf("%w: spec.reconciliation.retry_policy.max_retries must be between 0 and %d", ErrInvalidArgument, maxRetries) + } + if rp.BackoffSeconds != nil && (int(*rp.BackoffSeconds) < 1 || int(*rp.BackoffSeconds) > maxBackoffSeconds) { + return fmt.Errorf("%w: spec.reconciliation.retry_policy.backoff_seconds must be between 1 and %d", ErrInvalidArgument, maxBackoffSeconds) + } + } + return nil } func getOrGenerateID(clientID *string) string { diff --git a/pkg/gitops/client/client.gen.go b/pkg/gitops/client/client.gen.go index e62664c..6acb817 100644 --- a/pkg/gitops/client/client.gen.go +++ b/pkg/gitops/client/client.gen.go @@ -108,9 +108,6 @@ type ClientInterface interface { UpdateGitRepositoryWithBody(ctx context.Context, gitRepositoryId GitRepositoryIdPath, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) UpdateGitRepository(ctx context.Context, gitRepositoryId GitRepositoryIdPath, body UpdateGitRepositoryJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // SyncGitRepository request - SyncGitRepository(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*http.Response, error) } func (c *Client) ListGitRepositories(ctx context.Context, params *ListGitRepositoriesParams, reqEditors ...RequestEditorFn) (*http.Response, error) { @@ -197,18 +194,6 @@ func (c *Client) UpdateGitRepository(ctx context.Context, gitRepositoryId GitRep return c.Client.Do(req) } -func (c *Client) SyncGitRepository(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewSyncGitRepositoryRequest(c.Server, gitRepositoryId) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - // NewListGitRepositoriesRequest generates requests for ListGitRepositories func NewListGitRepositoriesRequest(server string, params *ListGitRepositoriesParams) (*http.Request, error) { var err error @@ -457,40 +442,6 @@ func NewUpdateGitRepositoryRequestWithBody(server string, gitRepositoryId GitRep return req, nil } -// NewSyncGitRepositoryRequest generates requests for SyncGitRepository -func NewSyncGitRepositoryRequest(server string, gitRepositoryId GitRepositoryIdPath) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "gitRepositoryId", gitRepositoryId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/git-repositories/%s:sync", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { for _, r := range c.RequestEditors { if err := r(ctx, req); err != nil { @@ -552,9 +503,6 @@ type ClientWithResponsesInterface interface { UpdateGitRepositoryWithBodyWithResponse(ctx context.Context, gitRepositoryId GitRepositoryIdPath, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpdateGitRepositoryResponse, error) UpdateGitRepositoryWithResponse(ctx context.Context, gitRepositoryId GitRepositoryIdPath, body UpdateGitRepositoryJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateGitRepositoryResponse, error) - - // SyncGitRepositoryWithResponse request - SyncGitRepositoryWithResponse(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*SyncGitRepositoryResponse, error) } type ListGitRepositoriesResponse struct { @@ -728,40 +676,6 @@ func (r UpdateGitRepositoryResponse) ContentType() string { return "" } -type SyncGitRepositoryResponse struct { - Body []byte - HTTPResponse *http.Response - JSON202 *GitRepositoryStatus - JSON401 *Unauthorized - JSON403 *Forbidden - JSON404 *NotFound - JSON500 *InternalServerError -} - -// Status returns HTTPResponse.Status -func (r SyncGitRepositoryResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r SyncGitRepositoryResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers -func (r SyncGitRepositoryResponse) ContentType() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Header.Get("Content-Type") - } - return "" -} - // ListGitRepositoriesWithResponse request returning *ListGitRepositoriesResponse func (c *ClientWithResponses) ListGitRepositoriesWithResponse(ctx context.Context, params *ListGitRepositoriesParams, reqEditors ...RequestEditorFn) (*ListGitRepositoriesResponse, error) { rsp, err := c.ListGitRepositories(ctx, params, reqEditors...) @@ -823,15 +737,6 @@ func (c *ClientWithResponses) UpdateGitRepositoryWithResponse(ctx context.Contex return ParseUpdateGitRepositoryResponse(rsp) } -// SyncGitRepositoryWithResponse request returning *SyncGitRepositoryResponse -func (c *ClientWithResponses) SyncGitRepositoryWithResponse(ctx context.Context, gitRepositoryId GitRepositoryIdPath, reqEditors ...RequestEditorFn) (*SyncGitRepositoryResponse, error) { - rsp, err := c.SyncGitRepository(ctx, gitRepositoryId, reqEditors...) - if err != nil { - return nil, err - } - return ParseSyncGitRepositoryResponse(rsp) -} - // ParseListGitRepositoriesResponse parses an HTTP response from a ListGitRepositoriesWithResponse call func ParseListGitRepositoriesResponse(rsp *http.Response) (*ListGitRepositoriesResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -1109,56 +1014,3 @@ func ParseUpdateGitRepositoryResponse(rsp *http.Response) (*UpdateGitRepositoryR return response, nil } -// ParseSyncGitRepositoryResponse parses an HTTP response from a SyncGitRepositoryWithResponse call -func ParseSyncGitRepositoryResponse(rsp *http.Response) (*SyncGitRepositoryResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &SyncGitRepositoryResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202: - var dest GitRepositoryStatus - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON202 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: - var dest Unauthorized - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON401 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: - var dest Forbidden - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON403 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: - var dest NotFound - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON404 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: - var dest InternalServerError - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON500 = &dest - - } - - return response, nil -} From c7523fb29d67589f79a3752d66c700c0f2851bd8 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Thu, 30 Jul 2026 13:51:02 +0200 Subject: [PATCH 09/14] Replace git CLI exec with go-git library Switch GitClient from shelling out to the git binary to using go-git, a pure Go git implementation. This eliminates the runtime dependency on the git CLI and allows removing git-core from the container image. The GitOperations interface is unchanged so all callers (reconciler, tests) continue to work without modification. Also adds a LatestRemoteCommit helper for lightweight remote polling without a full clone. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Ondra Machacek --- Containerfile.gitops | 3 - go.mod | 34 +++++-- go.sum | 97 ++++++++++++++++--- internal/gitops/controller/repository.go | 118 +++++++++++++++++------ 4 files changed, 195 insertions(+), 57 deletions(-) diff --git a/Containerfile.gitops b/Containerfile.gitops index 2223abb..8a92e09 100644 --- a/Containerfile.gitops +++ b/Containerfile.gitops @@ -14,9 +14,6 @@ RUN CGO_ENABLED=0 GOOS=linux go build -buildvcs=false -o dcm-gitops ./cmd/dcm-gi # Runtime stage FROM registry.access.redhat.com/ubi9/ubi-minimal:latest -# Install git (needed for clone/fetch operations) -RUN microdnf install -y git-core && microdnf clean all - WORKDIR /app COPY --from=builder /app/dcm-gitops . diff --git a/go.mod b/go.mod index d93dacf..654489c 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/coreos/go-oidc/v3 v3.20.0 github.com/getkin/kin-openapi v0.139.0 github.com/go-chi/chi/v5 v5.3.0 + github.com/go-git/go-git/v5 v5.19.2 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.8.0 github.com/kelseyhightower/envconfig v1.4.0 @@ -21,20 +22,29 @@ require ( github.com/open-policy-agent/opa v1.17.0 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 github.com/stretchr/testify v1.11.1 + gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.1 ) require ( + dario.cat/mergo v1.0.0 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/agnivade/levenshtein v1.2.1 // indirect github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.9.0 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-openapi/jsonpointer v0.22.4 // indirect @@ -42,6 +52,7 @@ require ( github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.6 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-tpm v0.9.8 // indirect github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect @@ -49,11 +60,14 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.18.5 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/lestrrat-go/blackmagic v1.0.4 // indirect github.com/lestrrat-go/dsig v1.2.1 // indirect github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect @@ -73,28 +87,32 @@ require ( github.com/oasdiff/yaml v0.1.0 // indirect github.com/oasdiff/yaml3 v0.0.13 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect github.com/segmentio/asm v1.2.1 // indirect + github.com/sergi/go-diff v1.4.0 // indirect github.com/sirupsen/logrus v1.9.4 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect github.com/tchap/go-patricia/v2 v2.3.3 // indirect github.com/valyala/fastjson v1.6.10 // indirect github.com/vektah/gqlparser/v2 v2.5.33 // indirect github.com/woodsbury/decimal128 v1.4.0 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yashtewari/glob-intersection v0.2.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.51.0 // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/net v0.53.0 // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/net v0.56.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.44.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.39.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.44.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + golang.org/x/tools v0.47.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index acc1789..2161049 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,27 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op h1:kpBdlEPbRvff0mDD1gk7o9BhI16b9p5yYAXRlidpqJE= github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= @@ -24,8 +35,12 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudevents/sdk-go/v2 v2.16.2 h1:ZYDFrYke4FD+jM8TZTJJO6JhKHzOQl2oqpFK1D+NnQM= github.com/cloudevents/sdk-go/v2 v2.16.2/go.mod h1:laOcGImm4nVJEU+PHnUrKL56CKmRL65RlQF0kRmW/kg= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/coreos/go-oidc/v3 v3.20.0 h1:EtE0WIBHk03N+DqGkY4+UONzzZHk7amKt6IyNd7OsZE= github.com/coreos/go-oidc/v3 v3.20.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -42,6 +57,10 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= @@ -54,8 +73,18 @@ github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZ github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM= github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= +github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY= +github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -78,6 +107,8 @@ github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -99,6 +130,8 @@ github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -112,10 +145,17 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= @@ -181,6 +221,10 @@ github.com/open-policy-agent/opa v1.17.0 h1:TMm6bCyb3CEL4wjXsXn1d/kBSBbjF+5sEIyz github.com/open-policy-agent/opa v1.17.0/go.mod h1:lcuZYSlqQpXFzsA6EJCELmfR5+nNOpZYX+eo7xaIIlk= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= +github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -202,11 +246,16 @@ github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -229,6 +278,8 @@ github.com/vektah/gqlparser/v2 v2.5.33 h1:lRp8aIeNUNbimf/axZd7ETg24q06hBtPaas+Tc github.com/vektah/gqlparser/v2 v2.5.33/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= @@ -247,30 +298,48 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/gitops/controller/repository.go b/internal/gitops/controller/repository.go index 839960e..00afdb7 100644 --- a/internal/gitops/controller/repository.go +++ b/internal/gitops/controller/repository.go @@ -4,10 +4,13 @@ import ( "context" "fmt" "os" - "os/exec" "path/filepath" "regexp" - "strings" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/storage/memory" ) // validRepoID matches DNS-1123 label format used for git repository IDs. @@ -23,7 +26,7 @@ type GitOperations interface { WorkDir(repoID string) string } -// GitClient implements GitOperations using the git CLI. +// GitClient implements GitOperations using go-git. type GitClient struct { baseDir string } @@ -37,50 +40,101 @@ func (g *GitClient) WorkDir(repoID string) string { return filepath.Join(g.baseDir, repoID) } -func (g *GitClient) CloneOrFetch(ctx context.Context, url, branch, repoID string) (string, error) { +func (g *GitClient) CloneOrFetch(ctx context.Context, repoURL, branch, repoID string) (string, error) { if !validRepoID.MatchString(repoID) { return "", fmt.Errorf("invalid repo ID %q: must match DNS-1123 label format", repoID) } dir := g.WorkDir(repoID) + refName := plumbing.NewBranchReferenceName(branch) if _, err := os.Stat(filepath.Join(dir, ".git")); os.IsNotExist(err) { - if err := os.MkdirAll(filepath.Dir(dir), 0o755); err != nil { - return "", fmt.Errorf("create parent dir: %w", err) - } - if err := g.run(ctx, g.baseDir, "git", "clone", "--branch", branch, "--single-branch", "--depth", "1", url, repoID); err != nil { - return "", fmt.Errorf("git clone: %w", err) - } - } else { - if err := g.run(ctx, dir, "git", "fetch", "origin", branch); err != nil { - return "", fmt.Errorf("git fetch: %w", err) - } - if err := g.run(ctx, dir, "git", "reset", "--hard", "origin/"+branch); err != nil { - return "", fmt.Errorf("git reset: %w", err) - } + return g.cloneRepo(ctx, repoURL, branch, dir, refName) + } + return g.fetchAndReset(ctx, branch, dir, refName) +} + +func (g *GitClient) cloneRepo(ctx context.Context, repoURL, branch, dir string, refName plumbing.ReferenceName) (string, error) { + if err := os.MkdirAll(filepath.Dir(dir), 0o755); err != nil { + return "", fmt.Errorf("create parent dir: %w", err) + } + + repo, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{ + URL: repoURL, + ReferenceName: refName, + SingleBranch: true, + Depth: 1, + }) + if err != nil { + return "", fmt.Errorf("git clone: %w", err) + } + + return headCommit(repo) +} + +func (g *GitClient) fetchAndReset(ctx context.Context, branch, dir string, refName plumbing.ReferenceName) (string, error) { + repo, err := git.PlainOpen(dir) + if err != nil { + return "", fmt.Errorf("open repo: %w", err) + } + + err = repo.FetchContext(ctx, &git.FetchOptions{ + RemoteName: "origin", + RefSpecs: []config.RefSpec{config.RefSpec(fmt.Sprintf("+refs/heads/%s:refs/remotes/origin/%s", branch, branch))}, + Depth: 1, + Force: true, + }) + if err != nil && err != git.NoErrAlreadyUpToDate { + return "", fmt.Errorf("git fetch: %w", err) + } + + // Resolve origin/ and reset HEAD to it + remoteRef, err := repo.Reference(plumbing.NewRemoteReferenceName("origin", branch), true) + if err != nil { + return "", fmt.Errorf("resolve remote ref: %w", err) } - commit, err := g.output(ctx, dir, "git", "rev-parse", "HEAD") + wt, err := repo.Worktree() if err != nil { - return "", fmt.Errorf("git rev-parse: %w", err) + return "", fmt.Errorf("worktree: %w", err) + } + + if err := wt.Reset(&git.ResetOptions{ + Commit: remoteRef.Hash(), + Mode: git.HardReset, + }); err != nil { + return "", fmt.Errorf("git reset: %w", err) } - return strings.TrimSpace(commit), nil + + return remoteRef.Hash().String(), nil } -func (g *GitClient) run(ctx context.Context, dir string, name string, args ...string) error { - cmd := exec.CommandContext(ctx, name, args...) - cmd.Dir = dir - cmd.Stdout = os.Stderr // log git output to stderr - cmd.Stderr = os.Stderr - return cmd.Run() +func headCommit(repo *git.Repository) (string, error) { + head, err := repo.Head() + if err != nil { + return "", fmt.Errorf("resolve HEAD: %w", err) + } + return head.Hash().String(), nil } -func (g *GitClient) output(ctx context.Context, dir string, name string, args ...string) (string, error) { - cmd := exec.CommandContext(ctx, name, args...) - cmd.Dir = dir - out, err := cmd.Output() +// LatestRemoteCommit resolves the latest commit on a remote branch without +// cloning the full repository. This can be used for lightweight polling. +func LatestRemoteCommit(ctx context.Context, repoURL, branch string) (string, error) { + remote := git.NewRemote(memory.NewStorage(), &config.RemoteConfig{ + Name: "origin", + URLs: []string{repoURL}, + }) + + refs, err := remote.ListContext(ctx, &git.ListOptions{}) if err != nil { - return "", err + return "", fmt.Errorf("ls-remote: %w", err) + } + + target := plumbing.NewBranchReferenceName(branch) + for _, ref := range refs { + if ref.Name() == target { + return ref.Hash().String(), nil + } } - return string(out), nil + return "", fmt.Errorf("branch %q not found on remote", branch) } From a2c70b5c46b41e0675dd185d1a3ac64f9556203c Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Thu, 30 Jul 2026 15:13:28 +0200 Subject: [PATCH 10/14] Clean up go-git repository: remove dead code and unused params Remove unused LatestRemoteCommit function, drop unused branch param from cloneRepo and unused refName param from fetchAndReset, and fix import ordering. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Ondra Machacek --- internal/gitops/app/run.go | 2 +- internal/gitops/controller/repository.go | 31 +++--------------------- 2 files changed, 5 insertions(+), 28 deletions(-) diff --git a/internal/gitops/app/run.go b/internal/gitops/app/run.go index 3011f24..807ffd4 100644 --- a/internal/gitops/app/run.go +++ b/internal/gitops/app/run.go @@ -24,8 +24,8 @@ import ( policyopa "github.com/dcm-project/control-plane/internal/policy/opa" policyservice "github.com/dcm-project/control-plane/internal/policy/service" policystore "github.com/dcm-project/control-plane/internal/policy/store" - spstore "github.com/dcm-project/control-plane/internal/sp/store" sprmsvc "github.com/dcm-project/control-plane/internal/sp/service/resource_manager" + spstore "github.com/dcm-project/control-plane/internal/sp/store" "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" diff --git a/internal/gitops/controller/repository.go b/internal/gitops/controller/repository.go index 00afdb7..7977994 100644 --- a/internal/gitops/controller/repository.go +++ b/internal/gitops/controller/repository.go @@ -10,7 +10,6 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/storage/memory" ) // validRepoID matches DNS-1123 label format used for git repository IDs. @@ -49,12 +48,12 @@ func (g *GitClient) CloneOrFetch(ctx context.Context, repoURL, branch, repoID st refName := plumbing.NewBranchReferenceName(branch) if _, err := os.Stat(filepath.Join(dir, ".git")); os.IsNotExist(err) { - return g.cloneRepo(ctx, repoURL, branch, dir, refName) + return g.cloneRepo(ctx, repoURL, dir, refName) } - return g.fetchAndReset(ctx, branch, dir, refName) + return g.fetchAndReset(ctx, branch, dir) } -func (g *GitClient) cloneRepo(ctx context.Context, repoURL, branch, dir string, refName plumbing.ReferenceName) (string, error) { +func (g *GitClient) cloneRepo(ctx context.Context, repoURL, dir string, refName plumbing.ReferenceName) (string, error) { if err := os.MkdirAll(filepath.Dir(dir), 0o755); err != nil { return "", fmt.Errorf("create parent dir: %w", err) } @@ -72,7 +71,7 @@ func (g *GitClient) cloneRepo(ctx context.Context, repoURL, branch, dir string, return headCommit(repo) } -func (g *GitClient) fetchAndReset(ctx context.Context, branch, dir string, refName plumbing.ReferenceName) (string, error) { +func (g *GitClient) fetchAndReset(ctx context.Context, branch, dir string) (string, error) { repo, err := git.PlainOpen(dir) if err != nil { return "", fmt.Errorf("open repo: %w", err) @@ -116,25 +115,3 @@ func headCommit(repo *git.Repository) (string, error) { } return head.Hash().String(), nil } - -// LatestRemoteCommit resolves the latest commit on a remote branch without -// cloning the full repository. This can be used for lightweight polling. -func LatestRemoteCommit(ctx context.Context, repoURL, branch string) (string, error) { - remote := git.NewRemote(memory.NewStorage(), &config.RemoteConfig{ - Name: "origin", - URLs: []string{repoURL}, - }) - - refs, err := remote.ListContext(ctx, &git.ListOptions{}) - if err != nil { - return "", fmt.Errorf("ls-remote: %w", err) - } - - target := plumbing.NewBranchReferenceName(branch) - for _, ref := range refs { - if ref.Name() == target { - return ref.Hash().String(), nil - } - } - return "", fmt.Errorf("branch %q not found on remote", branch) -} From f7439ae7116ee67291058c5fa4ffbb7f450ae762 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Thu, 30 Jul 2026 15:41:30 +0200 Subject: [PATCH 11/14] Fix lint issues in gitops packages Apply gofumpt formatting, add missing package comments, remove unused label constants and rename unused parameters to _. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Ondra Machacek --- internal/gitops/app/config.go | 5 +-- internal/gitops/controller/parser.go | 12 +++---- internal/gitops/controller/reconciler.go | 11 ++----- internal/gitops/handlers/v1alpha1/errors.go | 1 - internal/gitops/handlers/v1alpha1/handler.go | 1 - internal/gitops/service/service.go | 1 + internal/gitops/store/git_repository.go | 12 +++---- internal/gitops/store/model/git_repository.go | 31 ++++++++++--------- internal/gitops/store/store.go | 1 + 9 files changed, 35 insertions(+), 40 deletions(-) diff --git a/internal/gitops/app/config.go b/internal/gitops/app/config.go index 13193c3..70b9325 100644 --- a/internal/gitops/app/config.go +++ b/internal/gitops/app/config.go @@ -1,3 +1,4 @@ +// Package app implements the dcm-gitops controller process. package app import ( @@ -8,8 +9,8 @@ import ( // Config holds configuration for the dcm-gitops process. type Config struct { - Database DatabaseConfig - LogLevel string `envconfig:"LOG_LEVEL" default:"info"` + Database DatabaseConfig + LogLevel string `envconfig:"LOG_LEVEL" default:"info"` GitWorkDir string `envconfig:"GIT_WORK_DIR" default:"/tmp/dcm-gitops"` // PollInterval is how often (in seconds) the controller reloads the list of repos from the DB. PollInterval int `envconfig:"POLL_INTERVAL" default:"15"` diff --git a/internal/gitops/controller/parser.go b/internal/gitops/controller/parser.go index 0715c76..0d129e7 100644 --- a/internal/gitops/controller/parser.go +++ b/internal/gitops/controller/parser.go @@ -12,13 +12,13 @@ import ( // DesiredInstance represents a CatalogItemInstance parsed from a YAML file. type DesiredInstance struct { - Name string `yaml:"-"` - ApiVersion string `yaml:"-"` - DisplayName string `yaml:"-"` - CatalogItemID string `yaml:"-"` + Name string `yaml:"-"` + ApiVersion string `yaml:"-"` + DisplayName string `yaml:"-"` + CatalogItemID string `yaml:"-"` UserValues []DesiredUserValue `yaml:"-"` - SourceFile string `yaml:"-"` - Labels map[string]string `yaml:"-"` + SourceFile string `yaml:"-"` + Labels map[string]string `yaml:"-"` } // DesiredUserValue represents a user value in a desired instance YAML. diff --git a/internal/gitops/controller/reconciler.go b/internal/gitops/controller/reconciler.go index e6425a8..820a62f 100644 --- a/internal/gitops/controller/reconciler.go +++ b/internal/gitops/controller/reconciler.go @@ -12,13 +12,6 @@ import ( "github.com/dcm-project/control-plane/internal/gitops/store/model" ) -const ( - labelManagedBy = "gitops.dcm.io/managed-by" - labelRepository = "gitops.dcm.io/repository" - labelSourcePath = "gitops.dcm.io/source-path" - labelCommit = "gitops.dcm.io/commit" -) - // Reconciler handles the reconciliation of a single GitRepository. type Reconciler struct { gitopsStore store.Store @@ -151,7 +144,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, repo model.GitRepository) er return nil } -func (r *Reconciler) createInstance(ctx context.Context, repoID, commit string, desired DesiredInstance) error { +func (r *Reconciler) createInstance(ctx context.Context, _, _ string, desired DesiredInstance) error { apiVersion := desired.ApiVersion if apiVersion == "" { apiVersion = "v1alpha1" @@ -181,7 +174,7 @@ func (r *Reconciler) createInstance(ctx context.Context, repoID, commit string, return err } -func (r *Reconciler) listManagedInstances(ctx context.Context, repoID string) ([]catalogv1alpha1.CatalogItemInstance, error) { +func (r *Reconciler) listManagedInstances(ctx context.Context, _ string) ([]catalogv1alpha1.CatalogItemInstance, error) { // List all instances and filter by gitops labels. // A future optimization can add label-based filtering to the store. result, err := r.catalogSvc.List(ctx, catalogservice.CatalogItemInstanceListOptions{}) diff --git a/internal/gitops/handlers/v1alpha1/errors.go b/internal/gitops/handlers/v1alpha1/errors.go index 023fe68..ca9c248 100644 --- a/internal/gitops/handlers/v1alpha1/errors.go +++ b/internal/gitops/handlers/v1alpha1/errors.go @@ -103,4 +103,3 @@ func handleDeleteError(err error) server.DeleteGitRepositoryResponseObject { )), } } - diff --git a/internal/gitops/handlers/v1alpha1/handler.go b/internal/gitops/handlers/v1alpha1/handler.go index e892d4a..e0bd6a3 100644 --- a/internal/gitops/handlers/v1alpha1/handler.go +++ b/internal/gitops/handlers/v1alpha1/handler.go @@ -78,4 +78,3 @@ func (h *Handler) DeleteGitRepository(ctx context.Context, request server.Delete slog.InfoContext(ctx, "GitRepository deleted", "id", request.GitRepositoryId) return server.DeleteGitRepository204Response{}, nil } - diff --git a/internal/gitops/service/service.go b/internal/gitops/service/service.go index 1fd4a71..0bacf78 100644 --- a/internal/gitops/service/service.go +++ b/internal/gitops/service/service.go @@ -1,3 +1,4 @@ +// Package service implements the GitRepository business logic. package service import ( diff --git a/internal/gitops/store/git_repository.go b/internal/gitops/store/git_repository.go index 05230b0..7fc6de5 100644 --- a/internal/gitops/store/git_repository.go +++ b/internal/gitops/store/git_repository.go @@ -12,9 +12,9 @@ import ( ) var ( - ErrGitRepositoryNotFound = errors.New("git repository not found") - ErrGitRepositoryIDTaken = errors.New("git repository ID already taken") - ErrDisplayNameTaken = errors.New("display_name already taken") + ErrGitRepositoryNotFound = errors.New("git repository not found") + ErrGitRepositoryIDTaken = errors.New("git repository ID already taken") + ErrDisplayNameTaken = errors.New("display_name already taken") ) // GitRepositoryListOptions contains options for listing git repositories. @@ -149,10 +149,10 @@ func (s *GitRepositoryStore) UpdateSyncStatus(ctx context.Context, id, syncState result := s.db.WithContext(ctx).Model(&model.GitRepository{}). Where("id = ?", id). Updates(map[string]any{ - "sync_state": syncState, - "status_message": statusMessage, + "sync_state": syncState, + "status_message": statusMessage, "last_synced_commit": lastSyncedCommit, - "last_sync_time": &now, + "last_sync_time": &now, }) if result.Error != nil { return result.Error diff --git a/internal/gitops/store/model/git_repository.go b/internal/gitops/store/model/git_repository.go index f931768..4089f5b 100644 --- a/internal/gitops/store/model/git_repository.go +++ b/internal/gitops/store/model/git_repository.go @@ -1,3 +1,4 @@ +// Package model defines the GORM database models for gitops resources. package model import ( @@ -6,21 +7,21 @@ import ( // GitRepository represents a git repository configuration in the database. type GitRepository struct { - ID string `gorm:"column:id;primaryKey;type:varchar(63)"` - ApiVersion string `gorm:"column:api_version;not null"` - DisplayName string `gorm:"column:display_name;not null;uniqueIndex"` - URL string `gorm:"column:url;not null"` - Branch string `gorm:"column:branch;not null;default:main"` - Path string `gorm:"column:path;not null;default:."` - IntervalSeconds int `gorm:"column:interval_seconds;not null;default:60"` - MaxRetries int `gorm:"column:max_retries;not null;default:3"` - BackoffSeconds int `gorm:"column:backoff_seconds;not null;default:30"` - SyncState string `gorm:"column:sync_state;not null;default:PENDING"` - LastSyncedCommit string `gorm:"column:last_synced_commit"` - StatusMessage string `gorm:"column:status_message"` - LastSyncTime *time.Time `gorm:"column:last_sync_time"` - CreateTime time.Time `gorm:"column:create_time;autoCreateTime"` - UpdateTime time.Time `gorm:"column:update_time;autoUpdateTime"` + ID string `gorm:"column:id;primaryKey;type:varchar(63)"` + ApiVersion string `gorm:"column:api_version;not null"` + DisplayName string `gorm:"column:display_name;not null;uniqueIndex"` + URL string `gorm:"column:url;not null"` + Branch string `gorm:"column:branch;not null;default:main"` + Path string `gorm:"column:path;not null;default:."` + IntervalSeconds int `gorm:"column:interval_seconds;not null;default:60"` + MaxRetries int `gorm:"column:max_retries;not null;default:3"` + BackoffSeconds int `gorm:"column:backoff_seconds;not null;default:30"` + SyncState string `gorm:"column:sync_state;not null;default:PENDING"` + LastSyncedCommit string `gorm:"column:last_synced_commit"` + StatusMessage string `gorm:"column:status_message"` + LastSyncTime *time.Time `gorm:"column:last_sync_time"` + CreateTime time.Time `gorm:"column:create_time;autoCreateTime"` + UpdateTime time.Time `gorm:"column:update_time;autoUpdateTime"` } // GitRepositoryList is a slice of GitRepository for list results. diff --git a/internal/gitops/store/store.go b/internal/gitops/store/store.go index 1efafda..5489e75 100644 --- a/internal/gitops/store/store.go +++ b/internal/gitops/store/store.go @@ -1,3 +1,4 @@ +// Package store provides the data persistence layer for gitops resources. package store import "gorm.io/gorm" From df4c6cbde17a011c968cf0f2e4f595e9998c6fa5 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Tue, 11 Aug 2026 11:01:48 +0200 Subject: [PATCH 12/14] Fix ownership tracking, SSRF validation, and error handling in gitops - Add managed_instance table to track repo->instance ownership, preventing the reconciler from deleting instances it does not own - Inject gitops and user-defined labels on all catalog item resources, not just those referenced in user_values - Restrict git repository URLs to https/http/ssh schemes and block localhost, link-local, and cloud metadata endpoints - Preserve last_synced_commit on error instead of clearing it Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Ondra Machacek --- internal/gitops/app/run.go | 9 +- internal/gitops/{app => config}/config.go | 6 +- internal/gitops/controller/controller.go | 28 +++-- internal/gitops/controller/parser.go | 17 +++ internal/gitops/controller/reconciler.go | 101 +++++++++++------- internal/gitops/service/service.go | 59 ++++------ internal/gitops/store/git_repository.go | 19 ++-- internal/gitops/store/git_repository_test.go | 4 +- internal/gitops/store/managed_instance.go | 58 ++++++++++ internal/gitops/store/model/git_repository.go | 2 - .../gitops/store/model/managed_instance.go | 16 +++ internal/gitops/store/store.go | 15 ++- 12 files changed, 226 insertions(+), 108 deletions(-) rename internal/gitops/{app => config}/config.go (89%) create mode 100644 internal/gitops/store/managed_instance.go create mode 100644 internal/gitops/store/model/managed_instance.go diff --git a/internal/gitops/app/run.go b/internal/gitops/app/run.go index 807ffd4..4f9afbd 100644 --- a/internal/gitops/app/run.go +++ b/internal/gitops/app/run.go @@ -14,6 +14,7 @@ import ( catalogplacement "github.com/dcm-project/control-plane/internal/catalog/placement" catalogservice "github.com/dcm-project/control-plane/internal/catalog/service" catalogstore "github.com/dcm-project/control-plane/internal/catalog/store" + "github.com/dcm-project/control-plane/internal/gitops/config" "github.com/dcm-project/control-plane/internal/gitops/controller" gitopsstore "github.com/dcm-project/control-plane/internal/gitops/store" gitopsmodel "github.com/dcm-project/control-plane/internal/gitops/store/model" @@ -34,7 +35,7 @@ import ( // Run starts the dcm-gitops controller process. func Run() int { - cfg, err := LoadConfig() + cfg, err := config.LoadConfig() if err != nil { slog.Error("Failed to load configuration", "error", err) return 1 @@ -63,7 +64,7 @@ func Run() int { }() // Migrate gitops schema - if err := db.AutoMigrate(&gitopsmodel.GitRepository{}); err != nil { + if err := db.AutoMigrate(&gitopsmodel.GitRepository{}, &gitopsmodel.ManagedInstance{}); err != nil { slog.Error("Failed to migrate gitops schema", "error", err) return 1 } @@ -99,7 +100,7 @@ func Run() int { // Create git client and reconciler gitClient := controller.NewGitClient(cfg.GitWorkDir) - reconciler := controller.NewReconciler(gitopsDataStore, catalogSvc.CatalogItemInstance(), gitClient) + reconciler := controller.NewReconciler(gitopsDataStore, catalogSvc.CatalogItemInstance(), catalogSvc.CatalogItem(), gitClient) // Create and start controller pollInterval := time.Duration(cfg.PollInterval) * time.Second @@ -118,7 +119,7 @@ func Run() int { return 0 } -func openDB(cfg *Config) (*gorm.DB, error) { +func openDB(cfg *config.Config) (*gorm.DB, error) { var dialector gorm.Dialector switch cfg.Database.Type { case "pgsql": diff --git a/internal/gitops/app/config.go b/internal/gitops/config/config.go similarity index 89% rename from internal/gitops/app/config.go rename to internal/gitops/config/config.go index 70b9325..1ee5d96 100644 --- a/internal/gitops/app/config.go +++ b/internal/gitops/config/config.go @@ -1,5 +1,4 @@ -// Package app implements the dcm-gitops controller process. -package app +package config import ( "fmt" @@ -32,5 +31,8 @@ func LoadConfig() (*Config, error) { if err := envconfig.Process("", cfg); err != nil { return nil, fmt.Errorf("load config: %w", err) } + if cfg.PollInterval < 1 { + return nil, fmt.Errorf("POLL_INTERVAL must be >= 1 second, got %d", cfg.PollInterval) + } return cfg, nil } diff --git a/internal/gitops/controller/controller.go b/internal/gitops/controller/controller.go index b40e00f..ab03f1b 100644 --- a/internal/gitops/controller/controller.go +++ b/internal/gitops/controller/controller.go @@ -3,11 +3,13 @@ package controller import ( "context" + "fmt" "log/slog" "sync" "time" "github.com/dcm-project/control-plane/internal/gitops/store" + gitopsmodel "github.com/dcm-project/control-plane/internal/gitops/store/model" ) // Controller manages per-GitRepository reconciliation goroutines. @@ -25,7 +27,11 @@ type Controller struct { // NewController creates a new Controller. // pollInterval controls how often the controller reloads the list of GitRepositories from the DB. +// It panics if pollInterval is not positive, since time.NewTicker requires a positive duration. func NewController(reconciler *Reconciler, gitopsStore store.Store, pollInterval time.Duration) *Controller { + if pollInterval <= 0 { + panic(fmt.Sprintf("gitops controller: pollInterval must be positive, got %s", pollInterval)) + } return &Controller{ reconciler: reconciler, store: gitopsStore, @@ -92,16 +98,20 @@ func (c *Controller) reconcileAll(ctx context.Context) { } } - mu := c.repoLock(repo.ID) - if !mu.TryLock() { - slog.DebugContext(ctx, "Skipping repo, reconciliation already in progress", "repo_id", repo.ID) - continue - } + c.reconcileOne(ctx, repo) + } +} - if err := c.reconciler.Reconcile(ctx, repo); err != nil { - slog.ErrorContext(ctx, "Reconciliation failed", "repo_id", repo.ID, "error", err) - } - mu.Unlock() +func (c *Controller) reconcileOne(ctx context.Context, repo gitopsmodel.GitRepository) { + mu := c.repoLock(repo.ID) + if !mu.TryLock() { + slog.DebugContext(ctx, "Skipping repo, reconciliation already in progress", "repo_id", repo.ID) + return + } + defer mu.Unlock() + + if err := c.reconciler.Reconcile(ctx, repo); err != nil { + slog.ErrorContext(ctx, "Reconciliation failed", "repo_id", repo.ID, "error", err) } } diff --git a/internal/gitops/controller/parser.go b/internal/gitops/controller/parser.go index 0d129e7..f7eb429 100644 --- a/internal/gitops/controller/parser.go +++ b/internal/gitops/controller/parser.go @@ -68,6 +68,23 @@ func ParseCatalogItemInstances(dir, specPath string) ParseResult { fullPath := filepath.Join(dir, specPath) + // Verify the resolved path stays within the repo directory. + absDir, err := filepath.Abs(dir) + if err != nil { + result.Errors = append(result.Errors, ParseError{File: dir, Err: fmt.Errorf("resolve repo dir: %w", err)}) + return result + } + absFull, err := filepath.Abs(fullPath) + if err != nil { + result.Errors = append(result.Errors, ParseError{File: fullPath, Err: fmt.Errorf("resolve spec path: %w", err)}) + return result + } + rel, err := filepath.Rel(absDir, absFull) + if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + result.Errors = append(result.Errors, ParseError{File: specPath, Err: fmt.Errorf("spec.path escapes repository root")}) + return result + } + entries, err := os.ReadDir(fullPath) if err != nil { result.Errors = append(result.Errors, ParseError{File: fullPath, Err: err}) diff --git a/internal/gitops/controller/reconciler.go b/internal/gitops/controller/reconciler.go index 820a62f..3cff957 100644 --- a/internal/gitops/controller/reconciler.go +++ b/internal/gitops/controller/reconciler.go @@ -14,17 +14,19 @@ import ( // Reconciler handles the reconciliation of a single GitRepository. type Reconciler struct { - gitopsStore store.Store - catalogSvc catalogservice.CatalogItemInstanceService - gitClient GitOperations + gitopsStore store.Store + catalogSvc catalogservice.CatalogItemInstanceService + catalogItemSvc catalogservice.CatalogItemService + gitClient GitOperations } // NewReconciler creates a new Reconciler. -func NewReconciler(gitopsStore store.Store, catalogSvc catalogservice.CatalogItemInstanceService, gitClient GitOperations) *Reconciler { +func NewReconciler(gitopsStore store.Store, catalogSvc catalogservice.CatalogItemInstanceService, catalogItemSvc catalogservice.CatalogItemService, gitClient GitOperations) *Reconciler { return &Reconciler{ - gitopsStore: gitopsStore, - catalogSvc: catalogSvc, - gitClient: gitClient, + gitopsStore: gitopsStore, + catalogSvc: catalogSvc, + catalogItemSvc: catalogItemSvc, + gitClient: gitClient, } } @@ -67,8 +69,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, repo model.GitRepository) er return fmt.Errorf("all %d YAML files failed to parse", len(parseResult.Errors)) } - // 4. Get existing git-managed instances for this repo - existingInstances, err := r.listManagedInstances(ctx, repo.ID) + // 4. Get existing git-managed instance IDs for this repo + managedIDs, err := r.gitopsStore.ManagedInstance().ListByRepo(ctx, repo.ID) if err != nil { r.setError(ctx, repo.ID, fmt.Sprintf("failed to list managed instances: %s", err.Error())) return fmt.Errorf("list managed instances: %w", err) @@ -80,27 +82,23 @@ func (r *Reconciler) Reconcile(ctx context.Context, repo model.GitRepository) er desiredByName[d.Name] = d } - existingByName := make(map[string]string, len(existingInstances)) // name -> id - for _, inst := range existingInstances { - if inst.Uid == nil { - slog.WarnContext(ctx, "Skipping instance with nil UID", "id", repo.ID, "display_name", inst.DisplayName) - continue - } - existingByName[inst.DisplayName] = *inst.Uid + existingByID := make(map[string]struct{}, len(managedIDs)) + for _, id := range managedIDs { + existingByID[id] = struct{}{} } var toCreate []DesiredInstance for name, desired := range desiredByName { - if _, exists := existingByName[name]; !exists { + if _, exists := existingByID[name]; !exists { toCreate = append(toCreate, desired) } } var toDelete []string // IDs to delete - for name, id := range existingByName { - if _, exists := desiredByName[name]; !exists { + for id := range existingByID { + if _, exists := desiredByName[id]; !exists { toDelete = append(toDelete, id) - slog.InfoContext(ctx, "Will delete instance removed from Git", "id", repo.ID, "instance_name", name, "instance_id", id) + slog.InfoContext(ctx, "Will delete instance removed from Git", "id", repo.ID, "instance_id", id) } } @@ -120,6 +118,11 @@ func (r *Reconciler) Reconcile(ctx context.Context, repo model.GitRepository) er if err := r.createInstance(ctx, repo.ID, latestCommit, desired); err != nil { slog.ErrorContext(ctx, "Failed to create instance", "id", repo.ID, "instance_name", desired.Name, "error", err) reconcileErrors = append(reconcileErrors, fmt.Sprintf("create %s: %s", desired.Name, err.Error())) + continue + } + if err := r.gitopsStore.ManagedInstance().Add(ctx, repo.ID, desired.Name); err != nil { + slog.ErrorContext(ctx, "Failed to record managed instance", "id", repo.ID, "instance_name", desired.Name, "error", err) + reconcileErrors = append(reconcileErrors, fmt.Sprintf("record %s: %s", desired.Name, err.Error())) } } @@ -129,6 +132,11 @@ func (r *Reconciler) Reconcile(ctx context.Context, repo model.GitRepository) er if err := r.catalogSvc.Delete(ctx, instanceID); err != nil { slog.ErrorContext(ctx, "Failed to delete instance", "id", repo.ID, "instance_id", instanceID, "error", err) reconcileErrors = append(reconcileErrors, fmt.Sprintf("delete %s: %s", instanceID, err.Error())) + continue + } + if err := r.gitopsStore.ManagedInstance().Remove(ctx, repo.ID, instanceID); err != nil { + slog.ErrorContext(ctx, "Failed to remove managed instance record", "id", repo.ID, "instance_id", instanceID, "error", err) + reconcileErrors = append(reconcileErrors, fmt.Sprintf("unrecord %s: %s", instanceID, err.Error())) } } @@ -144,19 +152,47 @@ func (r *Reconciler) Reconcile(ctx context.Context, repo model.GitRepository) er return nil } -func (r *Reconciler) createInstance(ctx context.Context, _, _ string, desired DesiredInstance) error { +func (r *Reconciler) createInstance(ctx context.Context, repoID, latestCommit string, desired DesiredInstance) error { apiVersion := desired.ApiVersion if apiVersion == "" { apiVersion = "v1alpha1" } - userValues := make([]catalogv1alpha1.UserValue, len(desired.UserValues)) - for i, uv := range desired.UserValues { - userValues[i] = catalogv1alpha1.UserValue{ + // Look up catalog item to get all resource names + catalogItem, err := r.catalogItemSvc.Get(ctx, desired.CatalogItemID) + if err != nil { + return fmt.Errorf("get catalog item %s: %w", desired.CatalogItemID, err) + } + + // Merge gitops labels with user-defined labels from YAML metadata + mergedLabels := map[string]string{ + "gitops.dcm.io/repository": repoID, + "gitops.dcm.io/commit": latestCommit, + } + for k, v := range desired.Labels { + mergedLabels[k] = v + } + labelValue := map[string]interface{}{ + "labels": mergedLabels, + } + + // Inject labels for every resource in the catalog item + var userValues []catalogv1alpha1.UserValue + for _, resource := range catalogItem.Spec.Resources { + userValues = append(userValues, catalogv1alpha1.UserValue{ + Resource: resource.Name, + Path: "metadata", + Value: labelValue, + }) + } + + // Append the user's original values + for _, uv := range desired.UserValues { + userValues = append(userValues, catalogv1alpha1.UserValue{ Resource: uv.Resource, Path: uv.Path, Value: uv.Value, - } + }) } req := &catalogservice.CreateCatalogItemInstanceRequest{ @@ -170,23 +206,10 @@ func (r *Reconciler) createInstance(ctx context.Context, _, _ string, desired De // Use the desired instance name as the ID req.ID = &desired.Name - _, err := r.catalogSvc.Create(ctx, req) + _, err = r.catalogSvc.Create(ctx, req) return err } -func (r *Reconciler) listManagedInstances(ctx context.Context, _ string) ([]catalogv1alpha1.CatalogItemInstance, error) { - // List all instances and filter by gitops labels. - // A future optimization can add label-based filtering to the store. - result, err := r.catalogSvc.List(ctx, catalogservice.CatalogItemInstanceListOptions{}) - if err != nil { - return nil, err - } - - // TODO: filter by gitops labels once CatalogItemInstance supports labels. - // For now, return all instances — the reconciler will match by name. - return result.CatalogItemInstances, nil -} - func (r *Reconciler) setSynced(ctx context.Context, repoID, commit string) { if err := r.gitopsStore.GitRepository().UpdateSyncStatus(ctx, repoID, "SYNCED", "", commit); err != nil { slog.ErrorContext(ctx, "Failed to update sync status to SYNCED", "id", repoID, "error", err) diff --git a/internal/gitops/service/service.go b/internal/gitops/service/service.go index 0bacf78..b25e808 100644 --- a/internal/gitops/service/service.go +++ b/internal/gitops/service/service.go @@ -6,6 +6,8 @@ import ( "errors" "fmt" "net/url" + "path/filepath" + "strings" v1alpha1 "github.com/dcm-project/control-plane/api/gitops/v1alpha1" "github.com/dcm-project/control-plane/internal/gitops/store" @@ -16,8 +18,6 @@ import ( const ( minIntervalSeconds = 10 maxIntervalSeconds = 86400 - maxRetries = 100 - maxBackoffSeconds = 3600 ) var ( @@ -123,22 +123,35 @@ func validateGitRepository(req v1alpha1.GitRepository) error { if req.Spec.Url == "" { return fmt.Errorf("%w: spec.url is required", ErrInvalidArgument) } - if u, err := url.Parse(req.Spec.Url); err != nil || u.Scheme == "" || u.Host == "" { + u, err := url.Parse(req.Spec.Url) + if err != nil || u.Scheme == "" || u.Host == "" { return fmt.Errorf("%w: spec.url must be a valid URL with scheme and host", ErrInvalidArgument) } + switch u.Scheme { + case "https", "http", "ssh": + default: + return fmt.Errorf("%w: spec.url scheme must be https, http, or ssh", ErrInvalidArgument) + } + if u.Port() != "" { + return fmt.Errorf("%w: spec.url must not include a port", ErrInvalidArgument) + } + host := u.Hostname() + if host == "localhost" || strings.HasPrefix(host, "127.") || host == "::1" || host == "0.0.0.0" || host == "metadata.google.internal" || host == "169.254.169.254" { + return fmt.Errorf("%w: spec.url must not reference localhost or link-local addresses", ErrInvalidArgument) + } if req.Spec.IntervalSeconds != nil { v := int(*req.Spec.IntervalSeconds) if v < minIntervalSeconds || v > maxIntervalSeconds { return fmt.Errorf("%w: spec.interval_seconds must be between %d and %d", ErrInvalidArgument, minIntervalSeconds, maxIntervalSeconds) } } - if req.Spec.Reconciliation != nil && req.Spec.Reconciliation.RetryPolicy != nil { - rp := req.Spec.Reconciliation.RetryPolicy - if rp.MaxRetries != nil && (int(*rp.MaxRetries) < 0 || int(*rp.MaxRetries) > maxRetries) { - return fmt.Errorf("%w: spec.reconciliation.retry_policy.max_retries must be between 0 and %d", ErrInvalidArgument, maxRetries) + if req.Spec.Path != nil { + p := filepath.Clean(*req.Spec.Path) + if filepath.IsAbs(p) { + return fmt.Errorf("%w: spec.path must be a relative path", ErrInvalidArgument) } - if rp.BackoffSeconds != nil && (int(*rp.BackoffSeconds) < 1 || int(*rp.BackoffSeconds) > maxBackoffSeconds) { - return fmt.Errorf("%w: spec.reconciliation.retry_policy.backoff_seconds must be between 1 and %d", ErrInvalidArgument, maxBackoffSeconds) + if p == ".." || strings.HasPrefix(p, ".."+string(filepath.Separator)) { + return fmt.Errorf("%w: spec.path must not escape the repository root", ErrInvalidArgument) } } return nil @@ -179,23 +192,6 @@ func modelToAPI(m *model.GitRepository) v1alpha1.GitRepository { interval := int32(m.IntervalSeconds) repo.Spec.IntervalSeconds = &interval - maxRetries := int32(m.MaxRetries) - backoff := int32(m.BackoffSeconds) - repo.Spec.Reconciliation = &struct { - RetryPolicy *struct { - BackoffSeconds *int32 `json:"backoff_seconds,omitempty"` - MaxRetries *int32 `json:"max_retries,omitempty"` - } `json:"retry_policy,omitempty"` - }{ - RetryPolicy: &struct { - BackoffSeconds *int32 `json:"backoff_seconds,omitempty"` - MaxRetries *int32 `json:"max_retries,omitempty"` - }{ - MaxRetries: &maxRetries, - BackoffSeconds: &backoff, - }, - } - return repo } @@ -223,8 +219,6 @@ func apiToModel(id string, req v1alpha1.GitRepository) model.GitRepository { Branch: "main", Path: ".", IntervalSeconds: 60, - MaxRetries: 3, - BackoffSeconds: 30, SyncState: "PENDING", } @@ -237,15 +231,6 @@ func apiToModel(id string, req v1alpha1.GitRepository) model.GitRepository { if req.Spec.IntervalSeconds != nil { m.IntervalSeconds = int(*req.Spec.IntervalSeconds) } - if req.Spec.Reconciliation != nil && req.Spec.Reconciliation.RetryPolicy != nil { - if req.Spec.Reconciliation.RetryPolicy.MaxRetries != nil { - m.MaxRetries = int(*req.Spec.Reconciliation.RetryPolicy.MaxRetries) - } - if req.Spec.Reconciliation.RetryPolicy.BackoffSeconds != nil { - m.BackoffSeconds = int(*req.Spec.Reconciliation.RetryPolicy.BackoffSeconds) - } - } - return m } diff --git a/internal/gitops/store/git_repository.go b/internal/gitops/store/git_repository.go index 7fc6de5..8178efa 100644 --- a/internal/gitops/store/git_repository.go +++ b/internal/gitops/store/git_repository.go @@ -53,7 +53,7 @@ func (s *GitRepositoryStore) List(ctx context.Context, opts *GitRepositoryListOp var repos model.GitRepositoryList query := s.db.WithContext(ctx) - pageSize := 50 + pageSize := 100 if opts != nil && opts.PageSize > 0 { pageSize = opts.PageSize } @@ -121,7 +121,7 @@ func (s *GitRepositoryStore) Create(ctx context.Context, repo model.GitRepositor func (s *GitRepositoryStore) Update(ctx context.Context, repo model.GitRepository) (*model.GitRepository, error) { result := s.db.WithContext(ctx).Model(&repo). - Select("display_name", "url", "branch", "path", "interval_seconds", "max_retries", "backoff_seconds"). + Select("display_name", "url", "branch", "path", "interval_seconds"). Clauses(clause.Returning{}). Updates(&repo) if result.Error != nil { @@ -146,14 +146,17 @@ func (s *GitRepositoryStore) Delete(ctx context.Context, id string) error { func (s *GitRepositoryStore) UpdateSyncStatus(ctx context.Context, id, syncState, statusMessage, lastSyncedCommit string) error { now := time.Now().UTC() + updates := map[string]any{ + "sync_state": syncState, + "status_message": statusMessage, + "last_sync_time": &now, + } + if lastSyncedCommit != "" { + updates["last_synced_commit"] = lastSyncedCommit + } result := s.db.WithContext(ctx).Model(&model.GitRepository{}). Where("id = ?", id). - Updates(map[string]any{ - "sync_state": syncState, - "status_message": statusMessage, - "last_synced_commit": lastSyncedCommit, - "last_sync_time": &now, - }) + Updates(updates) if result.Error != nil { return result.Error } diff --git a/internal/gitops/store/git_repository_test.go b/internal/gitops/store/git_repository_test.go index 4a3449b..c2e84d3 100644 --- a/internal/gitops/store/git_repository_test.go +++ b/internal/gitops/store/git_repository_test.go @@ -23,7 +23,7 @@ var _ = Describe("GitRepositoryStore", func() { var err error db, err = gorm.Open(sqlite.Open(":memory:"), &gorm.Config{TranslateError: true}) Expect(err).NotTo(HaveOccurred()) - Expect(db.AutoMigrate(&model.GitRepository{})).To(Succeed()) + Expect(db.AutoMigrate(&model.GitRepository{}, &model.ManagedInstance{})).To(Succeed()) dataStore = store.NewStore(db) ctx = context.Background() }) @@ -213,8 +213,6 @@ var _ = Describe("GitRepositoryStore", func() { Branch: "develop", Path: "apps/", IntervalSeconds: 30, - MaxRetries: 5, - BackoffSeconds: 10, }) Expect(err).NotTo(HaveOccurred()) Expect(updated.DisplayName).To(Equal("Updated")) diff --git a/internal/gitops/store/managed_instance.go b/internal/gitops/store/managed_instance.go new file mode 100644 index 0000000..7d2468a --- /dev/null +++ b/internal/gitops/store/managed_instance.go @@ -0,0 +1,58 @@ +package store + +import ( + "context" + + "github.com/dcm-project/control-plane/internal/gitops/store/model" + "gorm.io/gorm" +) + +// ManagedInstance provides persistence for the repo->instance ownership mapping. +type ManagedInstance interface { + // ListByRepo returns all instance IDs managed by the given git repository. + ListByRepo(ctx context.Context, gitRepositoryID string) ([]string, error) + // Add records that the given instance is managed by the given git repository. + Add(ctx context.Context, gitRepositoryID, instanceID string) error + // Remove removes the ownership record for the given instance under the given repo. + Remove(ctx context.Context, gitRepositoryID, instanceID string) error +} + +type ManagedInstanceStore struct { + db *gorm.DB +} + +var _ ManagedInstance = (*ManagedInstanceStore)(nil) + +func NewManagedInstance(db *gorm.DB) ManagedInstance { + return &ManagedInstanceStore{db: db} +} + +func (s *ManagedInstanceStore) ListByRepo(ctx context.Context, gitRepositoryID string) ([]string, error) { + var ids []string + err := s.db.WithContext(ctx). + Model(&model.ManagedInstance{}). + Where("git_repository_id = ?", gitRepositoryID). + Order("instance_id ASC"). + Pluck("instance_id", &ids).Error + if err != nil { + return nil, err + } + if ids == nil { + ids = []string{} + } + return ids, nil +} + +func (s *ManagedInstanceStore) Add(ctx context.Context, gitRepositoryID, instanceID string) error { + record := model.ManagedInstance{ + GitRepositoryID: gitRepositoryID, + InstanceID: instanceID, + } + return s.db.WithContext(ctx).Create(&record).Error +} + +func (s *ManagedInstanceStore) Remove(ctx context.Context, gitRepositoryID, instanceID string) error { + return s.db.WithContext(ctx). + Where("git_repository_id = ? AND instance_id = ?", gitRepositoryID, instanceID). + Delete(&model.ManagedInstance{}).Error +} diff --git a/internal/gitops/store/model/git_repository.go b/internal/gitops/store/model/git_repository.go index 4089f5b..b64952d 100644 --- a/internal/gitops/store/model/git_repository.go +++ b/internal/gitops/store/model/git_repository.go @@ -14,8 +14,6 @@ type GitRepository struct { Branch string `gorm:"column:branch;not null;default:main"` Path string `gorm:"column:path;not null;default:."` IntervalSeconds int `gorm:"column:interval_seconds;not null;default:60"` - MaxRetries int `gorm:"column:max_retries;not null;default:3"` - BackoffSeconds int `gorm:"column:backoff_seconds;not null;default:30"` SyncState string `gorm:"column:sync_state;not null;default:PENDING"` LastSyncedCommit string `gorm:"column:last_synced_commit"` StatusMessage string `gorm:"column:status_message"` diff --git a/internal/gitops/store/model/managed_instance.go b/internal/gitops/store/model/managed_instance.go new file mode 100644 index 0000000..9bea16f --- /dev/null +++ b/internal/gitops/store/model/managed_instance.go @@ -0,0 +1,16 @@ +package model + +import "time" + +// ManagedInstance records that a CatalogItemInstance is managed by a GitRepository. +type ManagedInstance struct { + GitRepositoryID string `gorm:"column:git_repository_id;primaryKey;type:varchar(63)"` + InstanceID string `gorm:"column:instance_id;primaryKey;type:varchar(63)"` + CreateTime time.Time `gorm:"column:create_time;autoCreateTime"` + + GitRepositoryRef *GitRepository `gorm:"foreignKey:GitRepositoryID;references:ID;constraint:OnDelete:CASCADE"` +} + +func (ManagedInstance) TableName() string { + return "gitops_managed_instances" +} diff --git a/internal/gitops/store/store.go b/internal/gitops/store/store.go index 5489e75..405edf7 100644 --- a/internal/gitops/store/store.go +++ b/internal/gitops/store/store.go @@ -6,17 +6,20 @@ import "gorm.io/gorm" type Store interface { Close() error GitRepository() GitRepository + ManagedInstance() ManagedInstance } type DataStore struct { - db *gorm.DB - gitRepository GitRepository + db *gorm.DB + gitRepository GitRepository + managedInstance ManagedInstance } func NewStore(db *gorm.DB) Store { return &DataStore{ - db: db, - gitRepository: NewGitRepository(db), + db: db, + gitRepository: NewGitRepository(db), + managedInstance: NewManagedInstance(db), } } @@ -31,3 +34,7 @@ func (s *DataStore) Close() error { func (s *DataStore) GitRepository() GitRepository { return s.gitRepository } + +func (s *DataStore) ManagedInstance() ManagedInstance { + return s.managedInstance +} From ea00a0072f2c002eb8a90201dda410ae2bd1fc26 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Tue, 11 Aug 2026 11:17:17 +0200 Subject: [PATCH 13/14] Add missing package comments for staticcheck ST1000 Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Ondra Machacek --- internal/gitops/app/run.go | 1 + internal/gitops/config/config.go | 1 + 2 files changed, 2 insertions(+) diff --git a/internal/gitops/app/run.go b/internal/gitops/app/run.go index 4f9afbd..214d9b4 100644 --- a/internal/gitops/app/run.go +++ b/internal/gitops/app/run.go @@ -1,3 +1,4 @@ +// Package app implements the dcm-gitops controller process. package app import ( diff --git a/internal/gitops/config/config.go b/internal/gitops/config/config.go index 1ee5d96..d511ed2 100644 --- a/internal/gitops/config/config.go +++ b/internal/gitops/config/config.go @@ -1,3 +1,4 @@ +// Package config provides configuration loading for the dcm-gitops process. package config import ( From 699bdae12a8e2d324f96c9d2ddf98ad6b8c5637a Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Mon, 31 Aug 2026 17:07:51 +0200 Subject: [PATCH 14/14] Fix review comments Signed-off-by: Ondra Machacek --- api/gitops/v1alpha1/openapi.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/gitops/v1alpha1/openapi.yaml b/api/gitops/v1alpha1/openapi.yaml index 1b21796..6afd801 100644 --- a/api/gitops/v1alpha1/openapi.yaml +++ b/api/gitops/v1alpha1/openapi.yaml @@ -447,7 +447,6 @@ components: properties: type: type: string - format: uri-reference description: | Machine-readable error code. enum: @@ -481,7 +480,6 @@ components: instance: type: string - format: uri-reference description: | Unique identifier for this specific error occurrence. example: 7934df3e-4b63-429b-b0f5-b8d350ec165e