diff --git a/apiserver/controllers/controllers.go b/apiserver/controllers/controllers.go index d8750a504..2a57f9cfb 100644 --- a/apiserver/controllers/controllers.go +++ b/apiserver/controllers/controllers.go @@ -106,8 +106,15 @@ func (a *APIController) handleWorkflowJobEvent(ctx context.Context, w http.Respo signature := r.Header.Get("X-Hub-Signature-256") hookType := r.Header.Get("X-Github-Hook-Installation-Target-Type") + giteaTargetType := r.Header.Get("X-Gitea-Hook-Installation-Target-Type") - if err := a.r.DispatchWorkflowJob(hookType, signature, body); err != nil { + forgeType := runnerParams.GithubEndpointType + if giteaTargetType != "" { + forgeType = runnerParams.GiteaEndpointType + hookType = giteaTargetType + } + + if err := a.r.DispatchWorkflowJob(hookType, signature, forgeType, body); err != nil { switch { case errors.Is(err, gErrors.ErrNotFound): metrics.WebhooksReceived.WithLabelValues( diff --git a/apiserver/controllers/enterprises.go b/apiserver/controllers/enterprises.go index 9be1f1bc4..9ce278cd1 100644 --- a/apiserver/controllers/enterprises.go +++ b/apiserver/controllers/enterprises.go @@ -320,7 +320,7 @@ func (a *APIController) CreateEnterpriseScaleSetHandler(w http.ResponseWriter, r return } - scaleSet, err := a.r.CreateEntityScaleSet(ctx, runnerParams.GithubEntityTypeEnterprise, enterpriseID, scaleSetData) + scaleSet, err := a.r.CreateEntityScaleSet(ctx, runnerParams.ForgeEntityTypeEnterprise, enterpriseID, scaleSetData) if err != nil { slog.With(slog.Any("error", err)).ErrorContext(ctx, "error creating enterprise scale set") handleError(ctx, w, err) @@ -404,7 +404,7 @@ func (a *APIController) ListEnterpriseScaleSetsHandler(w http.ResponseWriter, r return } - scaleSets, err := a.r.ListEntityScaleSets(ctx, runnerParams.GithubEntityTypeEnterprise, enterpriseID) + scaleSets, err := a.r.ListEntityScaleSets(ctx, runnerParams.ForgeEntityTypeEnterprise, enterpriseID) if err != nil { slog.With(slog.Any("error", err)).ErrorContext(ctx, "listing scale sets") handleError(ctx, w, err) diff --git a/apiserver/controllers/gitea_credentials.go b/apiserver/controllers/gitea_credentials.go new file mode 100644 index 000000000..777be982a --- /dev/null +++ b/apiserver/controllers/gitea_credentials.go @@ -0,0 +1,241 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +package controllers + +import ( + "encoding/json" + "log/slog" + "math" + "net/http" + "strconv" + + "github.com/gorilla/mux" + + gErrors "github.com/cloudbase/garm-provider-common/errors" + "github.com/cloudbase/garm/params" +) + +// swagger:route GET /gitea/credentials credentials ListGiteaCredentials +// +// List all credentials. +// +// Responses: +// 200: Credentials +// 400: APIErrorResponse +func (a *APIController) ListGiteaCredentials(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + creds, err := a.r.ListGiteaCredentials(ctx) + if err != nil { + handleError(ctx, w, err) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(creds); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response") + } +} + +// swagger:route POST /gitea/credentials credentials CreateGiteaCredentials +// +// Create a Gitea credential. +// +// Parameters: +// + name: Body +// description: Parameters used when creating a Gitea credential. +// type: CreateGiteaCredentialsParams +// in: body +// required: true +// +// Responses: +// 200: ForgeCredentials +// 400: APIErrorResponse +func (a *APIController) CreateGiteaCredential(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var params params.CreateGiteaCredentialsParams + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to decode request") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + cred, err := a.r.CreateGiteaCredentials(ctx, params) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to create Gitea credential") + handleError(ctx, w, err) + return + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(cred); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response") + } +} + +// swagger:route GET /gitea/credentials/{id} credentials GetGiteaCredentials +// +// Get a Gitea credential. +// +// Parameters: +// + name: id +// description: ID of the Gitea credential. +// type: integer +// in: path +// required: true +// +// Responses: +// 200: ForgeCredentials +// 400: APIErrorResponse +func (a *APIController) GetGiteaCredential(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vars := mux.Vars(r) + idParam, ok := vars["id"] + if !ok { + slog.ErrorContext(ctx, "missing id in request") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + id, err := strconv.ParseUint(idParam, 10, 64) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to parse id") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + if id > math.MaxUint { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "id is too large") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + cred, err := a.r.GetGiteaCredentials(ctx, uint(id)) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to get Gitea credential") + handleError(ctx, w, err) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(cred); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response") + } +} + +// swagger:route DELETE /gitea/credentials/{id} credentials DeleteGiteaCredentials +// +// Delete a Gitea credential. +// +// Parameters: +// + name: id +// description: ID of the Gitea credential. +// type: integer +// in: path +// required: true +// +// Responses: +// default: APIErrorResponse +func (a *APIController) DeleteGiteaCredential(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vars := mux.Vars(r) + idParam, ok := vars["id"] + if !ok { + slog.ErrorContext(ctx, "missing id in request") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + id, err := strconv.ParseUint(idParam, 10, 64) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to parse id") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + if id > math.MaxUint { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "id is too large") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + if err := a.r.DeleteGiteaCredentials(ctx, uint(id)); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to delete Gitea credential") + handleError(ctx, w, err) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// swagger:route PUT /gitea/credentials/{id} credentials UpdateGiteaCredentials +// +// Update a Gitea credential. +// +// Parameters: +// + name: id +// description: ID of the Gitea credential. +// type: integer +// in: path +// required: true +// + name: Body +// description: Parameters used when updating a Gitea credential. +// type: UpdateGiteaCredentialsParams +// in: body +// required: true +// +// Responses: +// 200: ForgeCredentials +// 400: APIErrorResponse +func (a *APIController) UpdateGiteaCredential(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vars := mux.Vars(r) + idParam, ok := vars["id"] + if !ok { + slog.ErrorContext(ctx, "missing id in request") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + id, err := strconv.ParseUint(idParam, 10, 64) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to parse id") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + if id > math.MaxUint { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "id is too large") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + var params params.UpdateGiteaCredentialsParams + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to decode request") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + cred, err := a.r.UpdateGiteaCredentials(ctx, uint(id), params) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to update Gitea credential") + handleError(ctx, w, err) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(cred); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response") + } +} diff --git a/apiserver/controllers/gitea_endpoints.go b/apiserver/controllers/gitea_endpoints.go new file mode 100644 index 000000000..67e851781 --- /dev/null +++ b/apiserver/controllers/gitea_endpoints.go @@ -0,0 +1,199 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +package controllers + +import ( + "encoding/json" + "log/slog" + "net/http" + + "github.com/gorilla/mux" + + gErrors "github.com/cloudbase/garm-provider-common/errors" + "github.com/cloudbase/garm/params" +) + +// swagger:route POST /gitea/endpoints endpoints CreateGiteaEndpoint +// +// Create a Gitea Endpoint. +// +// Parameters: +// + name: Body +// description: Parameters used when creating a Gitea endpoint. +// type: CreateGiteaEndpointParams +// in: body +// required: true +// +// Responses: +// 200: ForgeEndpoint +// default: APIErrorResponse +func (a *APIController) CreateGiteaEndpoint(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var params params.CreateGiteaEndpointParams + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to decode request") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + endpoint, err := a.r.CreateGiteaEndpoint(ctx, params) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to create Gitea endpoint") + handleError(ctx, w, err) + return + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(endpoint); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response") + } +} + +// swagger:route GET /gitea/endpoints endpoints ListGiteaEndpoints +// +// List all Gitea Endpoints. +// +// Responses: +// 200: ForgeEndpoints +// default: APIErrorResponse +func (a *APIController) ListGiteaEndpoints(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + endpoints, err := a.r.ListGiteaEndpoints(ctx) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to list Gitea endpoints") + handleError(ctx, w, err) + return + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(endpoints); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response") + } +} + +// swagger:route GET /gitea/endpoints/{name} endpoints GetGiteaEndpoint +// +// Get a Gitea Endpoint. +// +// Parameters: +// + name: name +// description: The name of the Gitea endpoint. +// type: string +// in: path +// required: true +// +// Responses: +// 200: ForgeEndpoint +// default: APIErrorResponse +func (a *APIController) GetGiteaEndpoint(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + vars := mux.Vars(r) + name, ok := vars["name"] + if !ok { + slog.ErrorContext(ctx, "missing name in request") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + endpoint, err := a.r.GetGiteaEndpoint(ctx, name) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to get Gitea endpoint") + handleError(ctx, w, err) + return + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(endpoint); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response") + } +} + +// swagger:route DELETE /gitea/endpoints/{name} endpoints DeleteGiteaEndpoint +// +// Delete a Gitea Endpoint. +// +// Parameters: +// + name: name +// description: The name of the Gitea endpoint. +// type: string +// in: path +// required: true +// +// Responses: +// default: APIErrorResponse +func (a *APIController) DeleteGiteaEndpoint(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + vars := mux.Vars(r) + name, ok := vars["name"] + if !ok { + slog.ErrorContext(ctx, "missing name in request") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + if err := a.r.DeleteGiteaEndpoint(ctx, name); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to delete Gitea endpoint") + handleError(ctx, w, err) + return + } + w.WriteHeader(http.StatusNoContent) +} + +// swagger:route PUT /gitea/endpoints/{name} endpoints UpdateGiteaEndpoint +// +// Update a Gitea Endpoint. +// +// Parameters: +// + name: name +// description: The name of the Gitea endpoint. +// type: string +// in: path +// required: true +// + name: Body +// description: Parameters used when updating a Gitea endpoint. +// type: UpdateGiteaEndpointParams +// in: body +// required: true +// +// Responses: +// 200: ForgeEndpoint +// default: APIErrorResponse +func (a *APIController) UpdateGiteaEndpoint(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + vars := mux.Vars(r) + name, ok := vars["name"] + if !ok { + slog.ErrorContext(ctx, "missing name in request") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + var params params.UpdateGiteaEndpointParams + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to decode request") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + endpoint, err := a.r.UpdateGiteaEndpoint(ctx, name, params) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to update GitHub endpoint") + handleError(ctx, w, err) + return + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(endpoint); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response") + } +} diff --git a/apiserver/controllers/credentials.go b/apiserver/controllers/github_credentials.go similarity index 90% rename from apiserver/controllers/credentials.go rename to apiserver/controllers/github_credentials.go index 70869b546..04e087e51 100644 --- a/apiserver/controllers/credentials.go +++ b/apiserver/controllers/github_credentials.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package controllers import ( @@ -47,7 +60,7 @@ func (a *APIController) ListCredentials(w http.ResponseWriter, r *http.Request) // required: true // // Responses: -// 200: GithubCredentials +// 200: ForgeCredentials // 400: APIErrorResponse func (a *APIController) CreateGithubCredential(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -83,7 +96,7 @@ func (a *APIController) CreateGithubCredential(w http.ResponseWriter, r *http.Re // required: true // // Responses: -// 200: GithubCredentials +// 200: ForgeCredentials // 400: APIErrorResponse func (a *APIController) GetGithubCredential(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -183,7 +196,7 @@ func (a *APIController) DeleteGithubCredential(w http.ResponseWriter, r *http.Re // required: true // // Responses: -// 200: GithubCredentials +// 200: ForgeCredentials // 400: APIErrorResponse func (a *APIController) UpdateGithubCredential(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/apiserver/controllers/endpoints.go b/apiserver/controllers/github_endpoints.go similarity index 88% rename from apiserver/controllers/endpoints.go rename to apiserver/controllers/github_endpoints.go index 81e984d40..482f9d038 100644 --- a/apiserver/controllers/endpoints.go +++ b/apiserver/controllers/github_endpoints.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package controllers import ( @@ -23,7 +36,7 @@ import ( // required: true // // Responses: -// 200: GithubEndpoint +// 200: ForgeEndpoint // default: APIErrorResponse func (a *APIController) CreateGithubEndpoint(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -52,7 +65,7 @@ func (a *APIController) CreateGithubEndpoint(w http.ResponseWriter, r *http.Requ // List all GitHub Endpoints. // // Responses: -// 200: GithubEndpoints +// 200: ForgeEndpoints // default: APIErrorResponse func (a *APIController) ListGithubEndpoints(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -81,7 +94,7 @@ func (a *APIController) ListGithubEndpoints(w http.ResponseWriter, r *http.Reque // required: true // // Responses: -// 200: GithubEndpoint +// 200: ForgeEndpoint // default: APIErrorResponse func (a *APIController) GetGithubEndpoint(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -153,7 +166,7 @@ func (a *APIController) DeleteGithubEndpoint(w http.ResponseWriter, r *http.Requ // required: true // // Responses: -// 200: GithubEndpoint +// 200: ForgeEndpoint // default: APIErrorResponse func (a *APIController) UpdateGithubEndpoint(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/apiserver/controllers/organizations.go b/apiserver/controllers/organizations.go index 149dd4906..86f3c5d6d 100644 --- a/apiserver/controllers/organizations.go +++ b/apiserver/controllers/organizations.go @@ -330,7 +330,7 @@ func (a *APIController) CreateOrgScaleSetHandler(w http.ResponseWriter, r *http. return } - scaleSet, err := a.r.CreateEntityScaleSet(ctx, runnerParams.GithubEntityTypeOrganization, orgID, scalesetData) + scaleSet, err := a.r.CreateEntityScaleSet(ctx, runnerParams.ForgeEntityTypeOrganization, orgID, scalesetData) if err != nil { slog.With(slog.Any("error", err)).ErrorContext(ctx, "error creating organization scale set") handleError(ctx, w, err) @@ -414,7 +414,7 @@ func (a *APIController) ListOrgScaleSetsHandler(w http.ResponseWriter, r *http.R return } - scaleSets, err := a.r.ListEntityScaleSets(ctx, runnerParams.GithubEntityTypeOrganization, orgID) + scaleSets, err := a.r.ListEntityScaleSets(ctx, runnerParams.ForgeEntityTypeOrganization, orgID) if err != nil { slog.With(slog.Any("error", err)).ErrorContext(ctx, "listing scale sets") handleError(ctx, w, err) diff --git a/apiserver/controllers/repositories.go b/apiserver/controllers/repositories.go index 14693aac1..2eea0001a 100644 --- a/apiserver/controllers/repositories.go +++ b/apiserver/controllers/repositories.go @@ -329,7 +329,7 @@ func (a *APIController) CreateRepoScaleSetHandler(w http.ResponseWriter, r *http return } - scaleSet, err := a.r.CreateEntityScaleSet(ctx, runnerParams.GithubEntityTypeRepository, repoID, scaleSetData) + scaleSet, err := a.r.CreateEntityScaleSet(ctx, runnerParams.ForgeEntityTypeRepository, repoID, scaleSetData) if err != nil { slog.With(slog.Any("error", err)).ErrorContext(ctx, "error creating repository scale set") handleError(ctx, w, err) @@ -413,7 +413,7 @@ func (a *APIController) ListRepoScaleSetsHandler(w http.ResponseWriter, r *http. return } - scaleSets, err := a.r.ListEntityScaleSets(ctx, runnerParams.GithubEntityTypeRepository, repoID) + scaleSets, err := a.r.ListEntityScaleSets(ctx, runnerParams.ForgeEntityTypeRepository, repoID) if err != nil { slog.With(slog.Any("error", err)).ErrorContext(ctx, "listing scale sets") handleError(ctx, w, err) diff --git a/apiserver/events/events.go b/apiserver/events/events.go index 30e0b386b..94d707f2d 100644 --- a/apiserver/events/events.go +++ b/apiserver/events/events.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package events import ( diff --git a/apiserver/events/params.go b/apiserver/events/params.go index 274d3f1e4..a2b996a9e 100644 --- a/apiserver/events/params.go +++ b/apiserver/events/params.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package events import ( @@ -14,7 +27,7 @@ func (f Filter) Validate() error { case common.RepositoryEntityType, common.OrganizationEntityType, common.EnterpriseEntityType, common.PoolEntityType, common.UserEntityType, common.InstanceEntityType, common.JobEntityType, common.ControllerEntityType, common.GithubCredentialsEntityType, - common.GithubEndpointEntityType: + common.GiteaCredentialsEntityType, common.ScaleSetEntityType, common.GithubEndpointEntityType: default: return common.ErrInvalidEntityType } diff --git a/apiserver/routers/routers.go b/apiserver/routers/routers.go index ec1352921..2036b5f14 100644 --- a/apiserver/routers/routers.go +++ b/apiserver/routers/routers.go @@ -454,6 +454,44 @@ func NewAPIRouter(han *controllers.APIController, authMiddleware, initMiddleware apiRouter.Handle("/github/credentials/{id}/", http.HandlerFunc(han.UpdateGithubCredential)).Methods("PUT", "OPTIONS") apiRouter.Handle("/github/credentials/{id}", http.HandlerFunc(han.UpdateGithubCredential)).Methods("PUT", "OPTIONS") + ////////////////////// + // Gitea Endpoints // + ////////////////////// + // Create Gitea Endpoint + apiRouter.Handle("/gitea/endpoints/", http.HandlerFunc(han.CreateGiteaEndpoint)).Methods("POST", "OPTIONS") + apiRouter.Handle("/gitea/endpoints", http.HandlerFunc(han.CreateGiteaEndpoint)).Methods("POST", "OPTIONS") + // List Gitea Endpoints + apiRouter.Handle("/gitea/endpoints/", http.HandlerFunc(han.ListGiteaEndpoints)).Methods("GET", "OPTIONS") + apiRouter.Handle("/gitea/endpoints", http.HandlerFunc(han.ListGiteaEndpoints)).Methods("GET", "OPTIONS") + // Get Gitea Endpoint + apiRouter.Handle("/gitea/endpoints/{name}/", http.HandlerFunc(han.GetGiteaEndpoint)).Methods("GET", "OPTIONS") + apiRouter.Handle("/gitea/endpoints/{name}", http.HandlerFunc(han.GetGiteaEndpoint)).Methods("GET", "OPTIONS") + // Delete Gitea Endpoint + apiRouter.Handle("/gitea/endpoints/{name}/", http.HandlerFunc(han.DeleteGiteaEndpoint)).Methods("DELETE", "OPTIONS") + apiRouter.Handle("/gitea/endpoints/{name}", http.HandlerFunc(han.DeleteGiteaEndpoint)).Methods("DELETE", "OPTIONS") + // Update Gitea Endpoint + apiRouter.Handle("/gitea/endpoints/{name}/", http.HandlerFunc(han.UpdateGiteaEndpoint)).Methods("PUT", "OPTIONS") + apiRouter.Handle("/gitea/endpoints/{name}", http.HandlerFunc(han.UpdateGiteaEndpoint)).Methods("PUT", "OPTIONS") + + //////////////////////// + // Gitea credentials // + //////////////////////// + // List Gitea Credentials + apiRouter.Handle("/gitea/credentials/", http.HandlerFunc(han.ListGiteaCredentials)).Methods("GET", "OPTIONS") + apiRouter.Handle("/gitea/credentials", http.HandlerFunc(han.ListGiteaCredentials)).Methods("GET", "OPTIONS") + // Create Gitea Credentials + apiRouter.Handle("/gitea/credentials/", http.HandlerFunc(han.CreateGiteaCredential)).Methods("POST", "OPTIONS") + apiRouter.Handle("/gitea/credentials", http.HandlerFunc(han.CreateGiteaCredential)).Methods("POST", "OPTIONS") + // Get Gitea Credential + apiRouter.Handle("/gitea/credentials/{id}/", http.HandlerFunc(han.GetGiteaCredential)).Methods("GET", "OPTIONS") + apiRouter.Handle("/gitea/credentials/{id}", http.HandlerFunc(han.GetGiteaCredential)).Methods("GET", "OPTIONS") + // Delete Gitea Credential + apiRouter.Handle("/gitea/credentials/{id}/", http.HandlerFunc(han.DeleteGiteaCredential)).Methods("DELETE", "OPTIONS") + apiRouter.Handle("/gitea/credentials/{id}", http.HandlerFunc(han.DeleteGiteaCredential)).Methods("DELETE", "OPTIONS") + // Update Gitea Credential + apiRouter.Handle("/gitea/credentials/{id}/", http.HandlerFunc(han.UpdateGiteaCredential)).Methods("PUT", "OPTIONS") + apiRouter.Handle("/gitea/credentials/{id}", http.HandlerFunc(han.UpdateGiteaCredential)).Methods("PUT", "OPTIONS") + ///////////////////////// // Websocket endpoints // ///////////////////////// diff --git a/apiserver/swagger-models.yaml b/apiserver/swagger-models.yaml index ad83d6c85..74eaac84e 100644 --- a/apiserver/swagger-models.yaml +++ b/apiserver/swagger-models.yaml @@ -74,11 +74,11 @@ definitions: package: github.com/cloudbase/garm/params alias: garm_params items: - $ref: '#/definitions/GithubCredentials' - GithubCredentials: + $ref: '#/definitions/ForgeCredentials' + ForgeCredentials: type: object x-go-type: - type: GithubCredentials + type: ForgeCredentials import: package: github.com/cloudbase/garm/params alias: garm_params @@ -271,22 +271,29 @@ definitions: import: package: github.com/cloudbase/garm/params alias: garm_params - GithubEndpoint: + UpdateGiteaEndpointParams: type: object x-go-type: - type: GithubEndpoint + type: UpdateGiteaEndpointParams import: package: github.com/cloudbase/garm/params alias: garm_params - GithubEndpoints: + ForgeEndpoint: + type: object + x-go-type: + type: ForgeEndpoint + import: + package: github.com/cloudbase/garm/params + alias: garm_params + ForgeEndpoints: type: array x-go-type: - type: GithubEndpoints + type: ForgeEndpoints import: package: github.com/cloudbase/garm/params alias: garm_params items: - $ref: '#/definitions/GithubEndpoint' + $ref: '#/definitions/ForgeEndpoint' CreateGithubEndpointParams: type: object x-go-type: @@ -294,6 +301,13 @@ definitions: import: package: github.com/cloudbase/garm/params alias: garm_params + CreateGiteaEndpointParams: + type: object + x-go-type: + type: CreateGiteaEndpointParams + import: + package: github.com/cloudbase/garm/params + alias: garm_params CreateGithubCredentialsParams: type: object x-go-type: @@ -301,6 +315,13 @@ definitions: import: package: github.com/cloudbase/garm/params alias: garm_params + CreateGiteaCredentialsParams: + type: object + x-go-type: + type: CreateGiteaCredentialsParams + import: + package: github.com/cloudbase/garm/params + alias: garm_params UpdateGithubCredentialsParams: type: object x-go-type: @@ -308,6 +329,13 @@ definitions: import: package: github.com/cloudbase/garm/params alias: garm_params + UpdateGiteaCredentialsParams: + type: object + x-go-type: + type: UpdateGiteaCredentialsParams + import: + package: github.com/cloudbase/garm/params + alias: garm_params UpdateControllerParams: type: object x-go-type: diff --git a/apiserver/swagger.yaml b/apiserver/swagger.yaml index 2f89ab772..66e7a655a 100644 --- a/apiserver/swagger.yaml +++ b/apiserver/swagger.yaml @@ -23,6 +23,20 @@ definitions: alias: garm_params package: github.com/cloudbase/garm/params type: CreateEnterpriseParams + CreateGiteaCredentialsParams: + type: object + x-go-type: + import: + alias: garm_params + package: github.com/cloudbase/garm/params + type: CreateGiteaCredentialsParams + CreateGiteaEndpointParams: + type: object + x-go-type: + import: + alias: garm_params + package: github.com/cloudbase/garm/params + type: CreateGiteaEndpointParams CreateGithubCredentialsParams: type: object x-go-type: @@ -74,7 +88,7 @@ definitions: type: CreateScaleSetParams Credentials: items: - $ref: '#/definitions/GithubCredentials' + $ref: '#/definitions/ForgeCredentials' type: array x-go-type: import: @@ -97,29 +111,29 @@ definitions: alias: garm_params package: github.com/cloudbase/garm/params type: Enterprises - GithubCredentials: + ForgeCredentials: type: object x-go-type: import: alias: garm_params package: github.com/cloudbase/garm/params - type: GithubCredentials - GithubEndpoint: + type: ForgeCredentials + ForgeEndpoint: type: object x-go-type: import: alias: garm_params package: github.com/cloudbase/garm/params - type: GithubEndpoint - GithubEndpoints: + type: ForgeEndpoint + ForgeEndpoints: items: - $ref: '#/definitions/GithubEndpoint' + $ref: '#/definitions/ForgeEndpoint' type: array x-go-type: import: alias: garm_params package: github.com/cloudbase/garm/params - type: GithubEndpoints + type: ForgeEndpoints HookInfo: type: object x-go-type: @@ -281,6 +295,20 @@ definitions: alias: garm_params package: github.com/cloudbase/garm/params type: UpdateEntityParams + UpdateGiteaCredentialsParams: + type: object + x-go-type: + import: + alias: garm_params + package: github.com/cloudbase/garm/params + type: UpdateGiteaCredentialsParams + UpdateGiteaEndpointParams: + type: object + x-go-type: + import: + alias: garm_params + package: github.com/cloudbase/garm/params + type: UpdateGiteaEndpointParams UpdateGithubCredentialsParams: type: object x-go-type: @@ -721,6 +749,212 @@ paths: summary: Initialize the first run of the controller. tags: - first-run + /gitea/credentials: + get: + operationId: ListGiteaCredentials + responses: + "200": + description: Credentials + schema: + $ref: '#/definitions/Credentials' + "400": + description: APIErrorResponse + schema: + $ref: '#/definitions/APIErrorResponse' + summary: List all credentials. + tags: + - credentials + post: + operationId: CreateGiteaCredentials + parameters: + - description: Parameters used when creating a Gitea credential. + in: body + name: Body + required: true + schema: + $ref: '#/definitions/CreateGiteaCredentialsParams' + description: Parameters used when creating a Gitea credential. + type: object + responses: + "200": + description: ForgeCredentials + schema: + $ref: '#/definitions/ForgeCredentials' + "400": + description: APIErrorResponse + schema: + $ref: '#/definitions/APIErrorResponse' + summary: Create a Gitea credential. + tags: + - credentials + /gitea/credentials/{id}: + delete: + operationId: DeleteGiteaCredentials + parameters: + - description: ID of the Gitea credential. + in: path + name: id + required: true + type: integer + responses: + default: + description: APIErrorResponse + schema: + $ref: '#/definitions/APIErrorResponse' + summary: Delete a Gitea credential. + tags: + - credentials + get: + operationId: GetGiteaCredentials + parameters: + - description: ID of the Gitea credential. + in: path + name: id + required: true + type: integer + responses: + "200": + description: ForgeCredentials + schema: + $ref: '#/definitions/ForgeCredentials' + "400": + description: APIErrorResponse + schema: + $ref: '#/definitions/APIErrorResponse' + summary: Get a Gitea credential. + tags: + - credentials + put: + operationId: UpdateGiteaCredentials + parameters: + - description: ID of the Gitea credential. + in: path + name: id + required: true + type: integer + - description: Parameters used when updating a Gitea credential. + in: body + name: Body + required: true + schema: + $ref: '#/definitions/UpdateGiteaCredentialsParams' + description: Parameters used when updating a Gitea credential. + type: object + responses: + "200": + description: ForgeCredentials + schema: + $ref: '#/definitions/ForgeCredentials' + "400": + description: APIErrorResponse + schema: + $ref: '#/definitions/APIErrorResponse' + summary: Update a Gitea credential. + tags: + - credentials + /gitea/endpoints: + get: + operationId: ListGiteaEndpoints + responses: + "200": + description: ForgeEndpoints + schema: + $ref: '#/definitions/ForgeEndpoints' + default: + description: APIErrorResponse + schema: + $ref: '#/definitions/APIErrorResponse' + summary: List all Gitea Endpoints. + tags: + - endpoints + post: + operationId: CreateGiteaEndpoint + parameters: + - description: Parameters used when creating a Gitea endpoint. + in: body + name: Body + required: true + schema: + $ref: '#/definitions/CreateGiteaEndpointParams' + description: Parameters used when creating a Gitea endpoint. + type: object + responses: + "200": + description: ForgeEndpoint + schema: + $ref: '#/definitions/ForgeEndpoint' + default: + description: APIErrorResponse + schema: + $ref: '#/definitions/APIErrorResponse' + summary: Create a Gitea Endpoint. + tags: + - endpoints + /gitea/endpoints/{name}: + delete: + operationId: DeleteGiteaEndpoint + parameters: + - description: The name of the Gitea endpoint. + in: path + name: name + required: true + type: string + responses: + default: + description: APIErrorResponse + schema: + $ref: '#/definitions/APIErrorResponse' + summary: Delete a Gitea Endpoint. + tags: + - endpoints + get: + operationId: GetGiteaEndpoint + parameters: + - description: The name of the Gitea endpoint. + in: path + name: name + required: true + type: string + responses: + "200": + description: ForgeEndpoint + schema: + $ref: '#/definitions/ForgeEndpoint' + default: + description: APIErrorResponse + schema: + $ref: '#/definitions/APIErrorResponse' + summary: Get a Gitea Endpoint. + tags: + - endpoints + put: + operationId: UpdateGiteaEndpoint + parameters: + - description: The name of the Gitea endpoint. + in: path + name: name + required: true + type: string + - description: Parameters used when updating a Gitea endpoint. + in: body + name: Body + required: true + schema: + $ref: '#/definitions/UpdateGiteaEndpointParams' + description: Parameters used when updating a Gitea endpoint. + type: object + responses: + "200": + description: ForgeEndpoint + schema: + $ref: '#/definitions/ForgeEndpoint' + default: + description: APIErrorResponse + schema: + $ref: '#/definitions/APIErrorResponse' + summary: Update a Gitea Endpoint. + tags: + - endpoints /github/credentials: get: operationId: ListCredentials @@ -749,9 +983,9 @@ paths: type: object responses: "200": - description: GithubCredentials + description: ForgeCredentials schema: - $ref: '#/definitions/GithubCredentials' + $ref: '#/definitions/ForgeCredentials' "400": description: APIErrorResponse schema: @@ -786,9 +1020,9 @@ paths: type: integer responses: "200": - description: GithubCredentials + description: ForgeCredentials schema: - $ref: '#/definitions/GithubCredentials' + $ref: '#/definitions/ForgeCredentials' "400": description: APIErrorResponse schema: @@ -814,9 +1048,9 @@ paths: type: object responses: "200": - description: GithubCredentials + description: ForgeCredentials schema: - $ref: '#/definitions/GithubCredentials' + $ref: '#/definitions/ForgeCredentials' "400": description: APIErrorResponse schema: @@ -829,9 +1063,9 @@ paths: operationId: ListGithubEndpoints responses: "200": - description: GithubEndpoints + description: ForgeEndpoints schema: - $ref: '#/definitions/GithubEndpoints' + $ref: '#/definitions/ForgeEndpoints' default: description: APIErrorResponse schema: @@ -852,9 +1086,9 @@ paths: type: object responses: "200": - description: GithubEndpoint + description: ForgeEndpoint schema: - $ref: '#/definitions/GithubEndpoint' + $ref: '#/definitions/ForgeEndpoint' default: description: APIErrorResponse schema: @@ -889,9 +1123,9 @@ paths: type: string responses: "200": - description: GithubEndpoint + description: ForgeEndpoint schema: - $ref: '#/definitions/GithubEndpoint' + $ref: '#/definitions/ForgeEndpoint' default: description: APIErrorResponse schema: @@ -917,9 +1151,9 @@ paths: type: object responses: "200": - description: GithubEndpoint + description: ForgeEndpoint schema: - $ref: '#/definitions/GithubEndpoint' + $ref: '#/definitions/ForgeEndpoint' default: description: APIErrorResponse schema: diff --git a/auth/admin_required.go b/auth/admin_required.go index 8ab6cbacc..b3ca36244 100644 --- a/auth/admin_required.go +++ b/auth/admin_required.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package auth import "net/http" diff --git a/auth/context.go b/auth/context.go index 0d95be56a..1b648bb6e 100644 --- a/auth/context.go +++ b/auth/context.go @@ -44,8 +44,21 @@ const ( instanceTokenFetched contextFlags = "tokenFetched" instanceHasJITConfig contextFlags = "hasJITConfig" instanceParams contextFlags = "instanceParams" + instanceForgeTypeKey contextFlags = "forge_type" ) +func SetInstanceForgeType(ctx context.Context, val string) context.Context { + return context.WithValue(ctx, instanceForgeTypeKey, val) +} + +func InstanceForgeType(ctx context.Context) params.EndpointType { + elem := ctx.Value(instanceForgeTypeKey) + if elem == nil { + return "" + } + return elem.(params.EndpointType) +} + func SetInstanceID(ctx context.Context, id string) context.Context { return context.WithValue(ctx, instanceIDKey, id) } @@ -159,7 +172,7 @@ func InstanceEntity(ctx context.Context) string { return elem.(string) } -func PopulateInstanceContext(ctx context.Context, instance params.Instance) context.Context { +func PopulateInstanceContext(ctx context.Context, instance params.Instance, claims *InstanceJWTClaims) context.Context { ctx = SetInstanceID(ctx, instance.ID) ctx = SetInstanceName(ctx, instance.Name) ctx = SetInstancePoolID(ctx, instance.PoolID) @@ -167,6 +180,7 @@ func PopulateInstanceContext(ctx context.Context, instance params.Instance) cont ctx = SetInstanceTokenFetched(ctx, instance.TokenFetched) ctx = SetInstanceHasJITConfig(ctx, instance.JitConfiguration) ctx = SetInstanceParams(ctx, instance) + ctx = SetInstanceForgeType(ctx, claims.ForgeType) return ctx } diff --git a/auth/instance_middleware.go b/auth/instance_middleware.go index dbd3cfb78..bcae0b0a5 100644 --- a/auth/instance_middleware.go +++ b/auth/instance_middleware.go @@ -40,10 +40,11 @@ type InstanceJWTClaims struct { Name string `json:"name"` PoolID string `json:"provider_id"` // Scope is either repository or organization - Scope params.GithubEntityType `json:"scope"` + Scope params.ForgeEntityType `json:"scope"` // Entity is the repo or org name Entity string `json:"entity"` CreateAttempt int `json:"create_attempt"` + ForgeType string `json:"forge_type"` jwt.RegisteredClaims } @@ -60,7 +61,7 @@ type instanceToken struct { jwtSecret string } -func (i *instanceToken) NewInstanceJWTToken(instance params.Instance, entity string, entityType params.GithubEntityType, ttlMinutes uint) (string, error) { +func (i *instanceToken) NewInstanceJWTToken(instance params.Instance, entity params.ForgeEntity, entityType params.ForgeEntityType, ttlMinutes uint) (string, error) { // Token expiration is equal to the bootstrap timeout set on the pool plus the polling // interval garm uses to check for timed out runners. Runners that have not sent their info // by the end of this interval are most likely failed and will be reaped by garm anyway. @@ -83,7 +84,8 @@ func (i *instanceToken) NewInstanceJWTToken(instance params.Instance, entity str Name: instance.Name, PoolID: instance.PoolID, Scope: entityType, - Entity: entity, + Entity: entity.String(), + ForgeType: string(entity.Credentials.ForgeType), CreateAttempt: instance.CreateAttempt, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) @@ -124,7 +126,7 @@ func (amw *instanceMiddleware) claimsToContext(ctx context.Context, claims *Inst return ctx, runnerErrors.ErrUnauthorized } - ctx = PopulateInstanceContext(ctx, instanceInfo) + ctx = PopulateInstanceContext(ctx, instanceInfo, claims) return ctx, nil } diff --git a/auth/interfaces.go b/auth/interfaces.go index 4e4d370cd..ab68dbd79 100644 --- a/auth/interfaces.go +++ b/auth/interfaces.go @@ -26,5 +26,5 @@ type Middleware interface { } type InstanceTokenGetter interface { - NewInstanceJWTToken(instance params.Instance, entity string, poolType params.GithubEntityType, ttlMinutes uint) (string, error) + NewInstanceJWTToken(instance params.Instance, entity params.ForgeEntity, poolType params.ForgeEntityType, ttlMinutes uint) (string, error) } diff --git a/auth/metrics.go b/auth/metrics.go index 55cede440..5ea688e24 100644 --- a/auth/metrics.go +++ b/auth/metrics.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package auth import ( diff --git a/cache/cache_test.go b/cache/cache_test.go index 7a9773941..7a8ebed3f 100644 --- a/cache/cache_test.go +++ b/cache/cache_test.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package cache import ( @@ -6,6 +19,7 @@ import ( "github.com/stretchr/testify/suite" + runnerErrors "github.com/cloudbase/garm-provider-common/errors" commonParams "github.com/cloudbase/garm-provider-common/params" garmTesting "github.com/cloudbase/garm/internal/testing" "github.com/cloudbase/garm/params" @@ -13,15 +27,20 @@ import ( type CacheTestSuite struct { suite.Suite - entity params.GithubEntity + entity params.ForgeEntity } func (c *CacheTestSuite) SetupTest() { - c.entity = params.GithubEntity{ + c.entity = params.ForgeEntity{ ID: "1234", - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, Name: "test", Owner: "test", + Credentials: params.ForgeCredentials{ + ID: 1, + Name: "test", + ForgeType: params.GithubEndpointType, + }, } } @@ -30,7 +49,8 @@ func (c *CacheTestSuite) TearDownTest() { githubToolsCache.mux.Lock() defer githubToolsCache.mux.Unlock() githubToolsCache.entities = make(map[string]GithubEntityTools) - credentialsCache.cache = make(map[uint]params.GithubCredentials) + giteaCredentialsCache.cache = make(map[uint]params.ForgeCredentials) + credentialsCache.cache = make(map[uint]params.ForgeCredentials) instanceCache.cache = make(map[string]params.Instance) entityCache = &EntityCache{ entities: make(map[string]EntityItem), @@ -44,23 +64,55 @@ func (c *CacheTestSuite) TestCacheIsInitialized() { c.Require().NotNil(entityCache) } -func (c *CacheTestSuite) TestSetCacheWorks() { +func (c *CacheTestSuite) TestSetToolsCacheWorks() { tools := []commonParams.RunnerApplicationDownload{ { DownloadURL: garmTesting.Ptr("https://example.com"), }, } - c.Require().NotNil(githubToolsCache) c.Require().Len(githubToolsCache.entities, 0) SetGithubToolsCache(c.entity, tools) c.Require().Len(githubToolsCache.entities, 1) - cachedTools, ok := GetGithubToolsCache(c.entity.ID) - c.Require().True(ok) + cachedTools, err := GetGithubToolsCache(c.entity.ID) + c.Require().NoError(err) c.Require().Len(cachedTools, 1) c.Require().Equal(tools[0].GetDownloadURL(), cachedTools[0].GetDownloadURL()) } +func (c *CacheTestSuite) TestSetToolsCacheWithError() { + tools := []commonParams.RunnerApplicationDownload{ + { + DownloadURL: garmTesting.Ptr("https://example.com"), + }, + } + c.Require().NotNil(githubToolsCache) + c.Require().Len(githubToolsCache.entities, 0) + SetGithubToolsCache(c.entity, tools) + entity := githubToolsCache.entities[c.entity.ID] + + c.Require().Equal(int64(entity.expiresAt.Sub(entity.updatedAt).Minutes()), int64(60)) + c.Require().Len(githubToolsCache.entities, 1) + SetGithubToolsCacheError(c.entity, runnerErrors.ErrNotFound) + + cachedTools, err := GetGithubToolsCache(c.entity.ID) + c.Require().Error(err) + c.Require().Nil(cachedTools) +} + +func (c *CacheTestSuite) TestSetErrorOnNonExistingCacheEntity() { + entity := params.ForgeEntity{ + ID: "non-existing-entity", + } + c.Require().NotNil(githubToolsCache) + c.Require().Len(githubToolsCache.entities, 0) + SetGithubToolsCacheError(entity, runnerErrors.ErrNotFound) + + storedEntity, err := GetGithubToolsCache(entity.ID) + c.Require().Error(err) + c.Require().Nil(storedEntity) +} + func (c *CacheTestSuite) TestTimedOutToolsCache() { tools := []commonParams.RunnerApplicationDownload{ { @@ -71,26 +123,30 @@ func (c *CacheTestSuite) TestTimedOutToolsCache() { c.Require().NotNil(githubToolsCache) c.Require().Len(githubToolsCache.entities, 0) SetGithubToolsCache(c.entity, tools) - c.Require().Len(githubToolsCache.entities, 1) entity := githubToolsCache.entities[c.entity.ID] - entity.updatedAt = entity.updatedAt.Add(-2 * time.Hour) + + c.Require().Equal(int64(entity.expiresAt.Sub(entity.updatedAt).Minutes()), int64(60)) + c.Require().Len(githubToolsCache.entities, 1) + entity = githubToolsCache.entities[c.entity.ID] + entity.updatedAt = entity.updatedAt.Add(-3 * time.Hour) + entity.expiresAt = entity.updatedAt.Add(-2 * time.Hour) githubToolsCache.entities[c.entity.ID] = entity - cachedTools, ok := GetGithubToolsCache(c.entity.ID) - c.Require().False(ok) + cachedTools, err := GetGithubToolsCache(c.entity.ID) + c.Require().Error(err) c.Require().Nil(cachedTools) } func (c *CacheTestSuite) TestGetInexistentCache() { c.Require().NotNil(githubToolsCache) c.Require().Len(githubToolsCache.entities, 0) - cachedTools, ok := GetGithubToolsCache(c.entity.ID) - c.Require().False(ok) + cachedTools, err := GetGithubToolsCache(c.entity.ID) + c.Require().Error(err) c.Require().Nil(cachedTools) } func (c *CacheTestSuite) TestSetGithubCredentials() { - credentials := params.GithubCredentials{ + credentials := params.ForgeCredentials{ ID: 1, } SetGithubCredentials(credentials) @@ -100,7 +156,7 @@ func (c *CacheTestSuite) TestSetGithubCredentials() { } func (c *CacheTestSuite) TestGetGithubCredentials() { - credentials := params.GithubCredentials{ + credentials := params.ForgeCredentials{ ID: 1, } SetGithubCredentials(credentials) @@ -110,11 +166,11 @@ func (c *CacheTestSuite) TestGetGithubCredentials() { nonExisting, ok := GetGithubCredentials(2) c.Require().False(ok) - c.Require().Equal(params.GithubCredentials{}, nonExisting) + c.Require().Equal(params.ForgeCredentials{}, nonExisting) } func (c *CacheTestSuite) TestDeleteGithubCredentials() { - credentials := params.GithubCredentials{ + credentials := params.ForgeCredentials{ ID: 1, } SetGithubCredentials(credentials) @@ -125,14 +181,14 @@ func (c *CacheTestSuite) TestDeleteGithubCredentials() { DeleteGithubCredentials(1) cachedCreds, ok = GetGithubCredentials(1) c.Require().False(ok) - c.Require().Equal(params.GithubCredentials{}, cachedCreds) + c.Require().Equal(params.ForgeCredentials{}, cachedCreds) } func (c *CacheTestSuite) TestGetAllGithubCredentials() { - credentials1 := params.GithubCredentials{ + credentials1 := params.ForgeCredentials{ ID: 1, } - credentials2 := params.GithubCredentials{ + credentials2 := params.ForgeCredentials{ ID: 2, } SetGithubCredentials(credentials1) @@ -254,9 +310,9 @@ func (c *CacheTestSuite) TestGetInstancesForScaleSet() { } func (c *CacheTestSuite) TestSetGetEntityCache() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "test-entity", - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, Name: "test", Owner: "test", } @@ -265,22 +321,34 @@ func (c *CacheTestSuite) TestSetGetEntityCache() { c.Require().True(ok) c.Require().Equal(entity.ID, cachedEntity.ID) + pool := params.Pool{ + ID: "pool-1", + } + SetEntityPool(entity.ID, pool) + cachedEntityPools := GetEntityPools("test-entity") + c.Require().Equal(1, len(cachedEntityPools)) + entity.Credentials.Description = "test description" SetEntity(entity) cachedEntity, ok = GetEntity("test-entity") c.Require().True(ok) c.Require().Equal(entity.ID, cachedEntity.ID) c.Require().Equal(entity.Credentials.Description, cachedEntity.Credentials.Description) + + // Make sure we don't clobber pools after updating the entity + cachedEntityPools = GetEntityPools("test-entity") + c.Require().Equal(1, len(cachedEntityPools)) } func (c *CacheTestSuite) TestReplaceEntityPools() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "test-entity", - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, Name: "test", Owner: "test", - Credentials: params.GithubCredentials{ - ID: 1, + Credentials: params.ForgeCredentials{ + ID: 1, + ForgeType: params.GithubEndpointType, }, } pool1 := params.Pool{ @@ -290,9 +358,10 @@ func (c *CacheTestSuite) TestReplaceEntityPools() { ID: "pool-2", } - credentials := params.GithubCredentials{ - ID: 1, - Name: "test", + credentials := params.ForgeCredentials{ + ID: 1, + Name: "test", + ForgeType: params.GithubEndpointType, } SetGithubCredentials(credentials) @@ -310,9 +379,9 @@ func (c *CacheTestSuite) TestReplaceEntityPools() { } func (c *CacheTestSuite) TestReplaceEntityScaleSets() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "test-entity", - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, Name: "test", Owner: "test", } @@ -336,9 +405,9 @@ func (c *CacheTestSuite) TestReplaceEntityScaleSets() { } func (c *CacheTestSuite) TestDeleteEntity() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "test-entity", - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, Name: "test", Owner: "test", } @@ -350,13 +419,13 @@ func (c *CacheTestSuite) TestDeleteEntity() { DeleteEntity(entity.ID) cachedEntity, ok = GetEntity(entity.ID) c.Require().False(ok) - c.Require().Equal(params.GithubEntity{}, cachedEntity) + c.Require().Equal(params.ForgeEntity{}, cachedEntity) } func (c *CacheTestSuite) TestSetEntityPool() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "test-entity", - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, Name: "test", Owner: "test", } @@ -387,9 +456,9 @@ func (c *CacheTestSuite) TestSetEntityPool() { } func (c *CacheTestSuite) TestSetEntityScaleSet() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "test-entity", - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, Name: "test", Owner: "test", } @@ -417,9 +486,9 @@ func (c *CacheTestSuite) TestSetEntityScaleSet() { } func (c *CacheTestSuite) TestDeleteEntityPool() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "test-entity", - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, Name: "test", Owner: "test", } @@ -440,9 +509,9 @@ func (c *CacheTestSuite) TestDeleteEntityPool() { } func (c *CacheTestSuite) TestDeleteEntityScaleSet() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "test-entity", - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, Name: "test", Owner: "test", } @@ -463,9 +532,9 @@ func (c *CacheTestSuite) TestDeleteEntityScaleSet() { } func (c *CacheTestSuite) TestFindPoolsMatchingAllTags() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "test-entity", - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, Name: "test", Owner: "test", } @@ -520,9 +589,9 @@ func (c *CacheTestSuite) TestFindPoolsMatchingAllTags() { } func (c *CacheTestSuite) TestGetEntityPools() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "test-entity", - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, Name: "test", Owner: "test", } @@ -562,9 +631,9 @@ func (c *CacheTestSuite) TestGetEntityPools() { } func (c *CacheTestSuite) TestGetEntityScaleSet() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "test-entity", - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, Name: "test", Owner: "test", } @@ -584,9 +653,9 @@ func (c *CacheTestSuite) TestGetEntityScaleSet() { } func (c *CacheTestSuite) TestGetEntityPool() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "test-entity", - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, Name: "test", Owner: "test", } @@ -613,6 +682,358 @@ func (c *CacheTestSuite) TestGetEntityPool() { c.Require().Equal(pool.ID, poolFromCache.ID) } +func (c *CacheTestSuite) TestSetGiteaCredentials() { + credentials := params.ForgeCredentials{ + ID: 1, + Description: "test description", + } + SetGiteaCredentials(credentials) + cachedCreds, ok := GetGiteaCredentials(1) + c.Require().True(ok) + c.Require().Equal(credentials.ID, cachedCreds.ID) + + cachedCreds.Description = "new description" + SetGiteaCredentials(cachedCreds) + cachedCreds, ok = GetGiteaCredentials(1) + c.Require().True(ok) + c.Require().Equal(credentials.ID, cachedCreds.ID) + c.Require().Equal("new description", cachedCreds.Description) +} + +func (c *CacheTestSuite) TestGetAllGiteaCredentials() { + credentials1 := params.ForgeCredentials{ + ID: 1, + } + credentials2 := params.ForgeCredentials{ + ID: 2, + } + SetGiteaCredentials(credentials1) + SetGiteaCredentials(credentials2) + + cachedCreds := GetAllGiteaCredentials() + c.Require().Len(cachedCreds, 2) + c.Require().Contains(cachedCreds, credentials1) + c.Require().Contains(cachedCreds, credentials2) +} + +func (c *CacheTestSuite) TestDeleteGiteaCredentials() { + credentials := params.ForgeCredentials{ + ID: 1, + } + SetGiteaCredentials(credentials) + cachedCreds, ok := GetGiteaCredentials(1) + c.Require().True(ok) + c.Require().Equal(credentials.ID, cachedCreds.ID) + + DeleteGiteaCredentials(1) + cachedCreds, ok = GetGiteaCredentials(1) + c.Require().False(ok) + c.Require().Equal(params.ForgeCredentials{}, cachedCreds) +} + +func (c *CacheTestSuite) TestDeleteGiteaCredentialsNotFound() { + credentials := params.ForgeCredentials{ + ID: 1, + } + SetGiteaCredentials(credentials) + cachedCreds, ok := GetGiteaCredentials(1) + c.Require().True(ok) + c.Require().Equal(credentials.ID, cachedCreds.ID) + + DeleteGiteaCredentials(2) + cachedCreds, ok = GetGiteaCredentials(1) + c.Require().True(ok) + c.Require().Equal(credentials.ID, cachedCreds.ID) +} + +func (c *CacheTestSuite) TestUpdateCredentialsInAffectedEntities() { + credentials := params.ForgeCredentials{ + ID: 1, + Description: "test description", + } + entity1 := params.ForgeEntity{ + ID: "test-entity-1", + EntityType: params.ForgeEntityTypeOrganization, + Name: "test", + Owner: "test", + Credentials: credentials, + } + + entity2 := params.ForgeEntity{ + ID: "test-entity-2", + EntityType: params.ForgeEntityTypeOrganization, + Name: "test", + Owner: "test", + Credentials: credentials, + } + + SetEntity(entity1) + SetEntity(entity2) + + cachedEntity1, ok := GetEntity(entity1.ID) + c.Require().True(ok) + c.Require().Equal(entity1.ID, cachedEntity1.ID) + cachedEntity2, ok := GetEntity(entity2.ID) + c.Require().True(ok) + c.Require().Equal(entity2.ID, cachedEntity2.ID) + + c.Require().Equal(credentials.ID, cachedEntity1.Credentials.ID) + c.Require().Equal(credentials.ID, cachedEntity2.Credentials.ID) + c.Require().Equal(credentials.Description, cachedEntity1.Credentials.Description) + c.Require().Equal(credentials.Description, cachedEntity2.Credentials.Description) + + credentials.Description = "new description" + SetGiteaCredentials(credentials) + + cachedEntity1, ok = GetEntity(entity1.ID) + c.Require().True(ok) + c.Require().Equal(entity1.ID, cachedEntity1.ID) + cachedEntity2, ok = GetEntity(entity2.ID) + c.Require().True(ok) + c.Require().Equal(entity2.ID, cachedEntity2.ID) + + c.Require().Equal(credentials.ID, cachedEntity1.Credentials.ID) + c.Require().Equal(credentials.ID, cachedEntity2.Credentials.ID) + c.Require().Equal(credentials.Description, cachedEntity1.Credentials.Description) + c.Require().Equal(credentials.Description, cachedEntity2.Credentials.Description) +} + +func (c *CacheTestSuite) TestSetGiteaEntity() { + credentials := params.ForgeCredentials{ + ID: 1, + Description: "test description", + ForgeType: params.GiteaEndpointType, + } + entity := params.ForgeEntity{ + ID: "test-entity", + EntityType: params.ForgeEntityTypeOrganization, + Name: "test", + Owner: "test", + Credentials: credentials, + } + + SetGiteaCredentials(credentials) + SetEntity(entity) + + cachedEntity, ok := GetEntity(entity.ID) + c.Require().True(ok) + c.Require().Equal(entity.ID, cachedEntity.ID) + c.Require().Equal(credentials.ID, cachedEntity.Credentials.ID) + c.Require().Equal(credentials.Description, cachedEntity.Credentials.Description) + c.Require().Equal(credentials.ForgeType, cachedEntity.Credentials.ForgeType) +} + +func (c *CacheTestSuite) TestGetEntitiesUsingCredentials() { + credentials := params.ForgeCredentials{ + ID: 1, + Description: "test description", + Name: "test", + ForgeType: params.GithubEndpointType, + } + + credentials2 := params.ForgeCredentials{ + ID: 2, + Description: "test description2", + Name: "test", + ForgeType: params.GiteaEndpointType, + } + + entity1 := params.ForgeEntity{ + ID: "test-entity-1", + EntityType: params.ForgeEntityTypeOrganization, + Name: "test", + Owner: "test", + Credentials: credentials, + } + + entity2 := params.ForgeEntity{ + ID: "test-entity-2", + EntityType: params.ForgeEntityTypeOrganization, + Name: "test", + Owner: "test", + Credentials: credentials, + } + entity3 := params.ForgeEntity{ + ID: "test-entity-3", + EntityType: params.ForgeEntityTypeOrganization, + Name: "test", + Owner: "test", + Credentials: credentials2, + } + + SetEntity(entity1) + SetEntity(entity2) + SetEntity(entity3) + + cachedEntities := GetEntitiesUsingCredentials(credentials) + c.Require().Len(cachedEntities, 2) + c.Require().Contains(cachedEntities, entity1) + c.Require().Contains(cachedEntities, entity2) + + cachedEntities = GetEntitiesUsingCredentials(credentials2) + c.Require().Len(cachedEntities, 1) + c.Require().Contains(cachedEntities, entity3) +} + +func (c *CacheTestSuite) TestGetallEntities() { + credentials := params.ForgeCredentials{ + ID: 1, + Description: "test description", + Name: "test", + ForgeType: params.GithubEndpointType, + } + + credentials2 := params.ForgeCredentials{ + ID: 2, + Description: "test description2", + Name: "test", + ForgeType: params.GiteaEndpointType, + } + + entity1 := params.ForgeEntity{ + ID: "test-entity-1", + EntityType: params.ForgeEntityTypeOrganization, + Name: "test", + Owner: "test", + Credentials: credentials, + CreatedAt: time.Now(), + } + + entity2 := params.ForgeEntity{ + ID: "test-entity-2", + EntityType: params.ForgeEntityTypeOrganization, + Name: "test", + Owner: "test", + Credentials: credentials, + CreatedAt: time.Now().Add(1 * time.Second), + } + + entity3 := params.ForgeEntity{ + ID: "test-entity-3", + EntityType: params.ForgeEntityTypeOrganization, + Name: "test", + Owner: "test", + Credentials: credentials2, + CreatedAt: time.Now().Add(2 * time.Second), + } + + SetEntity(entity1) + SetEntity(entity2) + SetEntity(entity3) + + // Sorted by creation date + cachedEntities := GetAllEntities() + c.Require().Len(cachedEntities, 3) + c.Require().Equal(cachedEntities[0], entity1) + c.Require().Equal(cachedEntities[1], entity2) + c.Require().Equal(cachedEntities[2], entity3) +} + +func (c *CacheTestSuite) TestGetAllPools() { + entity := params.ForgeEntity{ + ID: "test-entity", + EntityType: params.ForgeEntityTypeOrganization, + Name: "test", + Owner: "test", + } + pool1 := params.Pool{ + ID: "pool-1", + CreatedAt: time.Now(), + Tags: []params.Tag{ + { + Name: "tag1", + }, + { + Name: "tag2", + }, + }, + } + + pool2 := params.Pool{ + ID: "pool-2", + CreatedAt: time.Now().Add(1 * time.Second), + Tags: []params.Tag{ + { + Name: "tag1", + }, + { + Name: "tag3", + }, + }, + } + + SetEntity(entity) + SetEntityPool(entity.ID, pool1) + SetEntityPool(entity.ID, pool2) + cachedEntity, ok := GetEntity(entity.ID) + c.Require().True(ok) + c.Require().Equal(entity.ID, cachedEntity.ID) + pools := GetAllPools() + c.Require().Len(pools, 2) + c.Require().Equal(pools[0].ID, pool1.ID) + c.Require().Equal(pools[1].ID, pool2.ID) +} + +func (c *CacheTestSuite) TestGetAllScaleSets() { + entity := params.ForgeEntity{ + ID: "test-entity", + EntityType: params.ForgeEntityTypeOrganization, + Name: "test", + Owner: "test", + } + scaleSet1 := params.ScaleSet{ + ID: 1, + } + scaleSet2 := params.ScaleSet{ + ID: 2, + } + + SetEntity(entity) + SetEntityScaleSet(entity.ID, scaleSet1) + SetEntityScaleSet(entity.ID, scaleSet2) + cachedEntity, ok := GetEntity(entity.ID) + c.Require().True(ok) + c.Require().Equal(entity.ID, cachedEntity.ID) + scaleSets := GetAllScaleSets() + c.Require().Len(scaleSets, 2) + c.Require().Equal(scaleSets[0].ID, scaleSet1.ID) + c.Require().Equal(scaleSets[1].ID, scaleSet2.ID) +} + +func (c *CacheTestSuite) TestGetAllGetAllGithubCredentialsAsMap() { + credentials1 := params.ForgeCredentials{ + ID: 1, + } + credentials2 := params.ForgeCredentials{ + ID: 2, + } + SetGithubCredentials(credentials1) + SetGithubCredentials(credentials2) + + cachedCreds := GetAllGithubCredentialsAsMap() + c.Require().Len(cachedCreds, 2) + c.Require().Contains(cachedCreds, credentials1.ID) + c.Require().Contains(cachedCreds, credentials2.ID) +} + +func (c *CacheTestSuite) TestGetAllGiteaCredentialsAsMap() { + credentials1 := params.ForgeCredentials{ + ID: 1, + CreatedAt: time.Now(), + } + credentials2 := params.ForgeCredentials{ + ID: 2, + CreatedAt: time.Now().Add(1 * time.Second), + } + SetGiteaCredentials(credentials1) + SetGiteaCredentials(credentials2) + + cachedCreds := GetAllGiteaCredentialsAsMap() + c.Require().Len(cachedCreds, 2) + c.Require().Contains(cachedCreds, credentials1.ID) + c.Require().Contains(cachedCreds, credentials2.ID) +} + func TestCacheTestSuite(t *testing.T) { t.Parallel() suite.Run(t, new(CacheTestSuite)) diff --git a/cache/credentials_cache.go b/cache/credentials_cache.go index 7cf65a03c..3cb5c71d8 100644 --- a/cache/credentials_cache.go +++ b/cache/credentials_cache.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package cache import ( @@ -6,32 +19,40 @@ import ( "github.com/cloudbase/garm/params" ) -var credentialsCache *GithubCredentials +var ( + credentialsCache *CredentialCache + giteaCredentialsCache *CredentialCache +) func init() { - ghCredentialsCache := &GithubCredentials{ - cache: make(map[uint]params.GithubCredentials), + ghCredentialsCache := &CredentialCache{ + cache: make(map[uint]params.ForgeCredentials), + } + gtCredentialsCache := &CredentialCache{ + cache: make(map[uint]params.ForgeCredentials), } + credentialsCache = ghCredentialsCache + giteaCredentialsCache = gtCredentialsCache } -type GithubCredentials struct { +type CredentialCache struct { mux sync.Mutex - cache map[uint]params.GithubCredentials + cache map[uint]params.ForgeCredentials } -func (g *GithubCredentials) SetCredentialsRateLimit(credsID uint, rateLimit params.GithubRateLimit) { +func (g *CredentialCache) SetCredentialsRateLimit(credsID uint, rateLimit params.GithubRateLimit) { g.mux.Lock() defer g.mux.Unlock() if creds, ok := g.cache[credsID]; ok { - creds.RateLimit = rateLimit + creds.RateLimit = &rateLimit g.cache[credsID] = creds } } -func (g *GithubCredentials) SetCredentials(credentials params.GithubCredentials) { +func (g *CredentialCache) SetCredentials(credentials params.ForgeCredentials) { g.mux.Lock() defer g.mux.Unlock() @@ -39,28 +60,28 @@ func (g *GithubCredentials) SetCredentials(credentials params.GithubCredentials) UpdateCredentialsInAffectedEntities(credentials) } -func (g *GithubCredentials) GetCredentials(id uint) (params.GithubCredentials, bool) { +func (g *CredentialCache) GetCredentials(id uint) (params.ForgeCredentials, bool) { g.mux.Lock() defer g.mux.Unlock() if creds, ok := g.cache[id]; ok { return creds, true } - return params.GithubCredentials{}, false + return params.ForgeCredentials{}, false } -func (g *GithubCredentials) DeleteCredentials(id uint) { +func (g *CredentialCache) DeleteCredentials(id uint) { g.mux.Lock() defer g.mux.Unlock() delete(g.cache, id) } -func (g *GithubCredentials) GetAllCredentials() []params.GithubCredentials { +func (g *CredentialCache) GetAllCredentials() []params.ForgeCredentials { g.mux.Lock() defer g.mux.Unlock() - creds := make([]params.GithubCredentials, 0, len(g.cache)) + creds := make([]params.ForgeCredentials, 0, len(g.cache)) for _, cred := range g.cache { creds = append(creds, cred) } @@ -70,11 +91,11 @@ func (g *GithubCredentials) GetAllCredentials() []params.GithubCredentials { return creds } -func (g *GithubCredentials) GetAllCredentialsAsMap() map[uint]params.GithubCredentials { +func (g *CredentialCache) GetAllCredentialsAsMap() map[uint]params.ForgeCredentials { g.mux.Lock() defer g.mux.Unlock() - creds := make(map[uint]params.GithubCredentials, len(g.cache)) + creds := make(map[uint]params.ForgeCredentials, len(g.cache)) for id, cred := range g.cache { creds[id] = cred } @@ -82,11 +103,11 @@ func (g *GithubCredentials) GetAllCredentialsAsMap() map[uint]params.GithubCrede return creds } -func SetGithubCredentials(credentials params.GithubCredentials) { +func SetGithubCredentials(credentials params.ForgeCredentials) { credentialsCache.SetCredentials(credentials) } -func GetGithubCredentials(id uint) (params.GithubCredentials, bool) { +func GetGithubCredentials(id uint) (params.ForgeCredentials, bool) { return credentialsCache.GetCredentials(id) } @@ -94,7 +115,7 @@ func DeleteGithubCredentials(id uint) { credentialsCache.DeleteCredentials(id) } -func GetAllGithubCredentials() []params.GithubCredentials { +func GetAllGithubCredentials() []params.ForgeCredentials { return credentialsCache.GetAllCredentials() } @@ -102,6 +123,26 @@ func SetCredentialsRateLimit(credsID uint, rateLimit params.GithubRateLimit) { credentialsCache.SetCredentialsRateLimit(credsID, rateLimit) } -func GetAllGithubCredentialsAsMap() map[uint]params.GithubCredentials { +func GetAllGithubCredentialsAsMap() map[uint]params.ForgeCredentials { return credentialsCache.GetAllCredentialsAsMap() } + +func SetGiteaCredentials(credentials params.ForgeCredentials) { + giteaCredentialsCache.SetCredentials(credentials) +} + +func GetGiteaCredentials(id uint) (params.ForgeCredentials, bool) { + return giteaCredentialsCache.GetCredentials(id) +} + +func DeleteGiteaCredentials(id uint) { + giteaCredentialsCache.DeleteCredentials(id) +} + +func GetAllGiteaCredentials() []params.ForgeCredentials { + return giteaCredentialsCache.GetAllCredentials() +} + +func GetAllGiteaCredentialsAsMap() map[uint]params.ForgeCredentials { + return giteaCredentialsCache.GetAllCredentialsAsMap() +} diff --git a/cache/entity_cache.go b/cache/entity_cache.go index 006f40dbf..4800dd9c1 100644 --- a/cache/entity_cache.go +++ b/cache/entity_cache.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package cache import ( @@ -16,7 +29,7 @@ func init() { } type EntityItem struct { - Entity params.GithubEntity + Entity params.ForgeEntity Pools map[string]params.Pool ScaleSets map[uint]params.ScaleSet } @@ -27,34 +40,40 @@ type EntityCache struct { entities map[string]EntityItem } -func (e *EntityCache) UpdateCredentialsInAffectedEntities(creds params.GithubCredentials) { +func (e *EntityCache) UpdateCredentialsInAffectedEntities(creds params.ForgeCredentials) { e.mux.Lock() defer e.mux.Unlock() for entityID, cache := range e.entities { - if cache.Entity.Credentials.ID == creds.ID { + if cache.Entity.Credentials.GetID() == creds.GetID() { cache.Entity.Credentials = creds e.entities[entityID] = cache } } } -func (e *EntityCache) GetEntity(entityID string) (params.GithubEntity, bool) { +func (e *EntityCache) GetEntity(entityID string) (params.ForgeEntity, bool) { e.mux.Lock() defer e.mux.Unlock() if cache, ok := e.entities[entityID]; ok { - // Get the credentials from the credentials cache. - creds, ok := GetGithubCredentials(cache.Entity.Credentials.ID) + var creds params.ForgeCredentials + var ok bool + switch cache.Entity.Credentials.ForgeType { + case params.GithubEndpointType: + creds, ok = GetGithubCredentials(cache.Entity.Credentials.ID) + case params.GiteaEndpointType: + creds, ok = GetGiteaCredentials(cache.Entity.Credentials.ID) + } if ok { cache.Entity.Credentials = creds } return cache.Entity, true } - return params.GithubEntity{}, false + return params.ForgeEntity{}, false } -func (e *EntityCache) SetEntity(entity params.GithubEntity) { +func (e *EntityCache) SetEntity(entity params.ForgeEntity) { e.mux.Lock() defer e.mux.Unlock() @@ -225,13 +244,17 @@ func (e *EntityCache) GetEntityScaleSets(entityID string) []params.ScaleSet { return nil } -func (e *EntityCache) GetEntitiesUsingGredentials(credsID uint) []params.GithubEntity { +func (e *EntityCache) GetEntitiesUsingCredentials(creds params.ForgeCredentials) []params.ForgeEntity { e.mux.Lock() defer e.mux.Unlock() - var entities []params.GithubEntity + var entities []params.ForgeEntity for _, cache := range e.entities { - if cache.Entity.Credentials.ID == credsID { + if cache.Entity.Credentials.ForgeType != creds.ForgeType { + continue + } + + if cache.Entity.Credentials.GetID() == creds.GetID() { entities = append(entities, cache.Entity) } } @@ -239,14 +262,21 @@ func (e *EntityCache) GetEntitiesUsingGredentials(credsID uint) []params.GithubE return entities } -func (e *EntityCache) GetAllEntities() []params.GithubEntity { +func (e *EntityCache) GetAllEntities() []params.ForgeEntity { e.mux.Lock() defer e.mux.Unlock() - var entities []params.GithubEntity + var entities []params.ForgeEntity for _, cache := range e.entities { // Get the credentials from the credentials cache. - creds, ok := GetGithubCredentials(cache.Entity.Credentials.ID) + var creds params.ForgeCredentials + var ok bool + switch cache.Entity.Credentials.ForgeType { + case params.GithubEndpointType: + creds, ok = GetGithubCredentials(cache.Entity.Credentials.ID) + case params.GiteaEndpointType: + creds, ok = GetGiteaCredentials(cache.Entity.Credentials.ID) + } if ok { cache.Entity.Credentials = creds } @@ -284,11 +314,11 @@ func (e *EntityCache) GetAllScaleSets() []params.ScaleSet { return scaleSets } -func GetEntity(entityID string) (params.GithubEntity, bool) { +func GetEntity(entityID string) (params.ForgeEntity, bool) { return entityCache.GetEntity(entityID) } -func SetEntity(entity params.GithubEntity) { +func SetEntity(entity params.ForgeEntity) { entityCache.SetEntity(entity) } @@ -340,15 +370,15 @@ func GetEntityScaleSets(entityID string) []params.ScaleSet { return entityCache.GetEntityScaleSets(entityID) } -func UpdateCredentialsInAffectedEntities(creds params.GithubCredentials) { +func UpdateCredentialsInAffectedEntities(creds params.ForgeCredentials) { entityCache.UpdateCredentialsInAffectedEntities(creds) } -func GetEntitiesUsingGredentials(credsID uint) []params.GithubEntity { - return entityCache.GetEntitiesUsingGredentials(credsID) +func GetEntitiesUsingCredentials(creds params.ForgeCredentials) []params.ForgeEntity { + return entityCache.GetEntitiesUsingCredentials(creds) } -func GetAllEntities() []params.GithubEntity { +func GetAllEntities() []params.ForgeEntity { return entityCache.GetAllEntities() } diff --git a/cache/github_client.go b/cache/github_client.go new file mode 100644 index 000000000..179a97186 --- /dev/null +++ b/cache/github_client.go @@ -0,0 +1,60 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +package cache + +import ( + "sync" + + "github.com/cloudbase/garm/runner/common" +) + +var ghClientCache *GithubClientCache + +type GithubClientCache struct { + mux sync.Mutex + + cache map[string]common.GithubClient +} + +func init() { + clientCache := &GithubClientCache{ + cache: make(map[string]common.GithubClient), + } + ghClientCache = clientCache +} + +func (g *GithubClientCache) SetClient(entityID string, client common.GithubClient) { + g.mux.Lock() + defer g.mux.Unlock() + + g.cache[entityID] = client +} + +func (g *GithubClientCache) GetClient(entityID string) (common.GithubClient, bool) { + g.mux.Lock() + defer g.mux.Unlock() + + if client, ok := g.cache[entityID]; ok { + return client, true + } + return nil, false +} + +func SetGithubClient(entityID string, client common.GithubClient) { + ghClientCache.SetClient(entityID, client) +} + +func GetGithubClient(entityID string) (common.GithubClient, bool) { + return ghClientCache.GetClient(entityID) +} diff --git a/cache/instance_cache.go b/cache/instance_cache.go index b96db5e9d..baf09945a 100644 --- a/cache/instance_cache.go +++ b/cache/instance_cache.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package cache import ( diff --git a/cache/tools_cache.go b/cache/tools_cache.go index 233de2c17..30e83a0e3 100644 --- a/cache/tools_cache.go +++ b/cache/tools_cache.go @@ -1,6 +1,20 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package cache import ( + "fmt" "sync" "time" @@ -19,7 +33,9 @@ func init() { type GithubEntityTools struct { updatedAt time.Time - entity params.GithubEntity + expiresAt time.Time + err error + entity params.ForgeEntity tools []commonParams.RunnerApplicationDownload } @@ -29,36 +45,72 @@ type GithubToolsCache struct { entities map[string]GithubEntityTools } -func (g *GithubToolsCache) Get(entityID string) ([]commonParams.RunnerApplicationDownload, bool) { +func (g *GithubToolsCache) Get(entityID string) ([]commonParams.RunnerApplicationDownload, error) { g.mux.Lock() defer g.mux.Unlock() if cache, ok := g.entities[entityID]; ok { - if time.Since(cache.updatedAt) > 1*time.Hour { - // Stale cache, remove it. - delete(g.entities, entityID) - return nil, false + if cache.entity.Credentials.ForgeType == params.GithubEndpointType { + if time.Now().UTC().After(cache.expiresAt.Add(-5 * time.Minute)) { + // Stale cache, remove it. + delete(g.entities, entityID) + return nil, fmt.Errorf("cache expired for entity %s", entityID) + } } - return cache.tools, true + if cache.err != nil { + return nil, cache.err + } + return cache.tools, nil } - return nil, false + return nil, fmt.Errorf("no cache found for entity %s", entityID) } -func (g *GithubToolsCache) Set(entity params.GithubEntity, tools []commonParams.RunnerApplicationDownload) { +func (g *GithubToolsCache) Set(entity params.ForgeEntity, tools []commonParams.RunnerApplicationDownload) { g.mux.Lock() defer g.mux.Unlock() - g.entities[entity.ID] = GithubEntityTools{ + forgeTools := GithubEntityTools{ updatedAt: time.Now(), entity: entity, tools: tools, + err: nil, + } + + if entity.Credentials.ForgeType == params.GithubEndpointType { + forgeTools.expiresAt = time.Now().Add(1 * time.Hour) + } + + g.entities[entity.ID] = forgeTools +} + +func (g *GithubToolsCache) SetToolsError(entity params.ForgeEntity, err error) { + g.mux.Lock() + defer g.mux.Unlock() + + // If the entity is not in the cache, add it with the error. + cache, ok := g.entities[entity.ID] + if !ok { + g.entities[entity.ID] = GithubEntityTools{ + updatedAt: time.Now(), + entity: entity, + err: err, + } + return } + + // Update the error for the existing entity. + cache.err = err + g.entities[entity.ID] = cache } -func SetGithubToolsCache(entity params.GithubEntity, tools []commonParams.RunnerApplicationDownload) { +func SetGithubToolsCache(entity params.ForgeEntity, tools []commonParams.RunnerApplicationDownload) { githubToolsCache.Set(entity, tools) } -func GetGithubToolsCache(entityID string) ([]commonParams.RunnerApplicationDownload, bool) { +func GetGithubToolsCache(entityID string) ([]commonParams.RunnerApplicationDownload, error) { return githubToolsCache.Get(entityID) } + +func SetGithubToolsCacheError(entity params.ForgeEntity, err error) { + githubToolsCache.SetToolsError(entity, err) +} diff --git a/cache/util.go b/cache/util.go index f8769c65c..5fd234a98 100644 --- a/cache/util.go +++ b/cache/util.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package cache import ( diff --git a/client/credentials/create_credentials_responses.go b/client/credentials/create_credentials_responses.go index cc5dc5dc8..a0037edf1 100644 --- a/client/credentials/create_credentials_responses.go +++ b/client/credentials/create_credentials_responses.go @@ -50,10 +50,10 @@ func NewCreateCredentialsOK() *CreateCredentialsOK { /* CreateCredentialsOK describes a response with status code 200, with default header values. -GithubCredentials +ForgeCredentials */ type CreateCredentialsOK struct { - Payload garm_params.GithubCredentials + Payload garm_params.ForgeCredentials } // IsSuccess returns true when this create credentials o k response has a 2xx status code @@ -96,7 +96,7 @@ func (o *CreateCredentialsOK) String() string { return fmt.Sprintf("[POST /github/credentials][%d] createCredentialsOK %s", 200, payload) } -func (o *CreateCredentialsOK) GetPayload() garm_params.GithubCredentials { +func (o *CreateCredentialsOK) GetPayload() garm_params.ForgeCredentials { return o.Payload } diff --git a/client/credentials/create_gitea_credentials_parameters.go b/client/credentials/create_gitea_credentials_parameters.go new file mode 100644 index 000000000..6e255bfaf --- /dev/null +++ b/client/credentials/create_gitea_credentials_parameters.go @@ -0,0 +1,151 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package credentials + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + + garm_params "github.com/cloudbase/garm/params" +) + +// NewCreateGiteaCredentialsParams creates a new CreateGiteaCredentialsParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewCreateGiteaCredentialsParams() *CreateGiteaCredentialsParams { + return &CreateGiteaCredentialsParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewCreateGiteaCredentialsParamsWithTimeout creates a new CreateGiteaCredentialsParams object +// with the ability to set a timeout on a request. +func NewCreateGiteaCredentialsParamsWithTimeout(timeout time.Duration) *CreateGiteaCredentialsParams { + return &CreateGiteaCredentialsParams{ + timeout: timeout, + } +} + +// NewCreateGiteaCredentialsParamsWithContext creates a new CreateGiteaCredentialsParams object +// with the ability to set a context for a request. +func NewCreateGiteaCredentialsParamsWithContext(ctx context.Context) *CreateGiteaCredentialsParams { + return &CreateGiteaCredentialsParams{ + Context: ctx, + } +} + +// NewCreateGiteaCredentialsParamsWithHTTPClient creates a new CreateGiteaCredentialsParams object +// with the ability to set a custom HTTPClient for a request. +func NewCreateGiteaCredentialsParamsWithHTTPClient(client *http.Client) *CreateGiteaCredentialsParams { + return &CreateGiteaCredentialsParams{ + HTTPClient: client, + } +} + +/* +CreateGiteaCredentialsParams contains all the parameters to send to the API endpoint + + for the create gitea credentials operation. + + Typically these are written to a http.Request. +*/ +type CreateGiteaCredentialsParams struct { + + /* Body. + + Parameters used when creating a Gitea credential. + */ + Body garm_params.CreateGiteaCredentialsParams + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the create gitea credentials params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *CreateGiteaCredentialsParams) WithDefaults() *CreateGiteaCredentialsParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the create gitea credentials params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *CreateGiteaCredentialsParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the create gitea credentials params +func (o *CreateGiteaCredentialsParams) WithTimeout(timeout time.Duration) *CreateGiteaCredentialsParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the create gitea credentials params +func (o *CreateGiteaCredentialsParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the create gitea credentials params +func (o *CreateGiteaCredentialsParams) WithContext(ctx context.Context) *CreateGiteaCredentialsParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the create gitea credentials params +func (o *CreateGiteaCredentialsParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the create gitea credentials params +func (o *CreateGiteaCredentialsParams) WithHTTPClient(client *http.Client) *CreateGiteaCredentialsParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the create gitea credentials params +func (o *CreateGiteaCredentialsParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithBody adds the body to the create gitea credentials params +func (o *CreateGiteaCredentialsParams) WithBody(body garm_params.CreateGiteaCredentialsParams) *CreateGiteaCredentialsParams { + o.SetBody(body) + return o +} + +// SetBody adds the body to the create gitea credentials params +func (o *CreateGiteaCredentialsParams) SetBody(body garm_params.CreateGiteaCredentialsParams) { + o.Body = body +} + +// WriteToRequest writes these params to a swagger request +func (o *CreateGiteaCredentialsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + if err := r.SetBodyParam(o.Body); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/client/credentials/create_gitea_credentials_responses.go b/client/credentials/create_gitea_credentials_responses.go new file mode 100644 index 000000000..2389cb043 --- /dev/null +++ b/client/credentials/create_gitea_credentials_responses.go @@ -0,0 +1,179 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package credentials + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + apiserver_params "github.com/cloudbase/garm/apiserver/params" + garm_params "github.com/cloudbase/garm/params" +) + +// CreateGiteaCredentialsReader is a Reader for the CreateGiteaCredentials structure. +type CreateGiteaCredentialsReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *CreateGiteaCredentialsReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewCreateGiteaCredentialsOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 400: + result := NewCreateGiteaCredentialsBadRequest() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + default: + return nil, runtime.NewAPIError("[POST /gitea/credentials] CreateGiteaCredentials", response, response.Code()) + } +} + +// NewCreateGiteaCredentialsOK creates a CreateGiteaCredentialsOK with default headers values +func NewCreateGiteaCredentialsOK() *CreateGiteaCredentialsOK { + return &CreateGiteaCredentialsOK{} +} + +/* +CreateGiteaCredentialsOK describes a response with status code 200, with default header values. + +ForgeCredentials +*/ +type CreateGiteaCredentialsOK struct { + Payload garm_params.ForgeCredentials +} + +// IsSuccess returns true when this create gitea credentials o k response has a 2xx status code +func (o *CreateGiteaCredentialsOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this create gitea credentials o k response has a 3xx status code +func (o *CreateGiteaCredentialsOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this create gitea credentials o k response has a 4xx status code +func (o *CreateGiteaCredentialsOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this create gitea credentials o k response has a 5xx status code +func (o *CreateGiteaCredentialsOK) IsServerError() bool { + return false +} + +// IsCode returns true when this create gitea credentials o k response a status code equal to that given +func (o *CreateGiteaCredentialsOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the create gitea credentials o k response +func (o *CreateGiteaCredentialsOK) Code() int { + return 200 +} + +func (o *CreateGiteaCredentialsOK) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /gitea/credentials][%d] createGiteaCredentialsOK %s", 200, payload) +} + +func (o *CreateGiteaCredentialsOK) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /gitea/credentials][%d] createGiteaCredentialsOK %s", 200, payload) +} + +func (o *CreateGiteaCredentialsOK) GetPayload() garm_params.ForgeCredentials { + return o.Payload +} + +func (o *CreateGiteaCredentialsOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewCreateGiteaCredentialsBadRequest creates a CreateGiteaCredentialsBadRequest with default headers values +func NewCreateGiteaCredentialsBadRequest() *CreateGiteaCredentialsBadRequest { + return &CreateGiteaCredentialsBadRequest{} +} + +/* +CreateGiteaCredentialsBadRequest describes a response with status code 400, with default header values. + +APIErrorResponse +*/ +type CreateGiteaCredentialsBadRequest struct { + Payload apiserver_params.APIErrorResponse +} + +// IsSuccess returns true when this create gitea credentials bad request response has a 2xx status code +func (o *CreateGiteaCredentialsBadRequest) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this create gitea credentials bad request response has a 3xx status code +func (o *CreateGiteaCredentialsBadRequest) IsRedirect() bool { + return false +} + +// IsClientError returns true when this create gitea credentials bad request response has a 4xx status code +func (o *CreateGiteaCredentialsBadRequest) IsClientError() bool { + return true +} + +// IsServerError returns true when this create gitea credentials bad request response has a 5xx status code +func (o *CreateGiteaCredentialsBadRequest) IsServerError() bool { + return false +} + +// IsCode returns true when this create gitea credentials bad request response a status code equal to that given +func (o *CreateGiteaCredentialsBadRequest) IsCode(code int) bool { + return code == 400 +} + +// Code gets the status code for the create gitea credentials bad request response +func (o *CreateGiteaCredentialsBadRequest) Code() int { + return 400 +} + +func (o *CreateGiteaCredentialsBadRequest) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /gitea/credentials][%d] createGiteaCredentialsBadRequest %s", 400, payload) +} + +func (o *CreateGiteaCredentialsBadRequest) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /gitea/credentials][%d] createGiteaCredentialsBadRequest %s", 400, payload) +} + +func (o *CreateGiteaCredentialsBadRequest) GetPayload() apiserver_params.APIErrorResponse { + return o.Payload +} + +func (o *CreateGiteaCredentialsBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/client/credentials/credentials_client.go b/client/credentials/credentials_client.go index 9d7b05637..3dfe1abd1 100644 --- a/client/credentials/credentials_client.go +++ b/client/credentials/credentials_client.go @@ -58,14 +58,24 @@ type ClientOption func(*runtime.ClientOperation) type ClientService interface { CreateCredentials(params *CreateCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*CreateCredentialsOK, error) + CreateGiteaCredentials(params *CreateGiteaCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*CreateGiteaCredentialsOK, error) + DeleteCredentials(params *DeleteCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) error + DeleteGiteaCredentials(params *DeleteGiteaCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) error + GetCredentials(params *GetCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetCredentialsOK, error) + GetGiteaCredentials(params *GetGiteaCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetGiteaCredentialsOK, error) + ListCredentials(params *ListCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*ListCredentialsOK, error) + ListGiteaCredentials(params *ListGiteaCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*ListGiteaCredentialsOK, error) + UpdateCredentials(params *UpdateCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UpdateCredentialsOK, error) + UpdateGiteaCredentials(params *UpdateGiteaCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UpdateGiteaCredentialsOK, error) + SetTransport(transport runtime.ClientTransport) } @@ -108,6 +118,45 @@ func (a *Client) CreateCredentials(params *CreateCredentialsParams, authInfo run panic(msg) } +/* +CreateGiteaCredentials creates a gitea credential +*/ +func (a *Client) CreateGiteaCredentials(params *CreateGiteaCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*CreateGiteaCredentialsOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewCreateGiteaCredentialsParams() + } + op := &runtime.ClientOperation{ + ID: "CreateGiteaCredentials", + Method: "POST", + PathPattern: "/gitea/credentials", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &CreateGiteaCredentialsReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*CreateGiteaCredentialsOK) + if ok { + return success, nil + } + // unexpected success response + // safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for CreateGiteaCredentials: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + /* DeleteCredentials deletes a git hub credential */ @@ -140,6 +189,38 @@ func (a *Client) DeleteCredentials(params *DeleteCredentialsParams, authInfo run return nil } +/* +DeleteGiteaCredentials deletes a gitea credential +*/ +func (a *Client) DeleteGiteaCredentials(params *DeleteGiteaCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) error { + // TODO: Validate the params before sending + if params == nil { + params = NewDeleteGiteaCredentialsParams() + } + op := &runtime.ClientOperation{ + ID: "DeleteGiteaCredentials", + Method: "DELETE", + PathPattern: "/gitea/credentials/{id}", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &DeleteGiteaCredentialsReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + _, err := a.transport.Submit(op) + if err != nil { + return err + } + return nil +} + /* GetCredentials gets a git hub credential */ @@ -179,6 +260,45 @@ func (a *Client) GetCredentials(params *GetCredentialsParams, authInfo runtime.C panic(msg) } +/* +GetGiteaCredentials gets a gitea credential +*/ +func (a *Client) GetGiteaCredentials(params *GetGiteaCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetGiteaCredentialsOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewGetGiteaCredentialsParams() + } + op := &runtime.ClientOperation{ + ID: "GetGiteaCredentials", + Method: "GET", + PathPattern: "/gitea/credentials/{id}", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &GetGiteaCredentialsReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*GetGiteaCredentialsOK) + if ok { + return success, nil + } + // unexpected success response + // safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for GetGiteaCredentials: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + /* ListCredentials lists all credentials */ @@ -218,6 +338,45 @@ func (a *Client) ListCredentials(params *ListCredentialsParams, authInfo runtime panic(msg) } +/* +ListGiteaCredentials lists all credentials +*/ +func (a *Client) ListGiteaCredentials(params *ListGiteaCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*ListGiteaCredentialsOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewListGiteaCredentialsParams() + } + op := &runtime.ClientOperation{ + ID: "ListGiteaCredentials", + Method: "GET", + PathPattern: "/gitea/credentials", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &ListGiteaCredentialsReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*ListGiteaCredentialsOK) + if ok { + return success, nil + } + // unexpected success response + // safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for ListGiteaCredentials: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + /* UpdateCredentials updates a git hub credential */ @@ -257,6 +416,45 @@ func (a *Client) UpdateCredentials(params *UpdateCredentialsParams, authInfo run panic(msg) } +/* +UpdateGiteaCredentials updates a gitea credential +*/ +func (a *Client) UpdateGiteaCredentials(params *UpdateGiteaCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UpdateGiteaCredentialsOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewUpdateGiteaCredentialsParams() + } + op := &runtime.ClientOperation{ + ID: "UpdateGiteaCredentials", + Method: "PUT", + PathPattern: "/gitea/credentials/{id}", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &UpdateGiteaCredentialsReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*UpdateGiteaCredentialsOK) + if ok { + return success, nil + } + // unexpected success response + // safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for UpdateGiteaCredentials: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + // SetTransport changes the transport on the client func (a *Client) SetTransport(transport runtime.ClientTransport) { a.transport = transport diff --git a/client/credentials/delete_gitea_credentials_parameters.go b/client/credentials/delete_gitea_credentials_parameters.go new file mode 100644 index 000000000..598ac477c --- /dev/null +++ b/client/credentials/delete_gitea_credentials_parameters.go @@ -0,0 +1,152 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package credentials + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// NewDeleteGiteaCredentialsParams creates a new DeleteGiteaCredentialsParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewDeleteGiteaCredentialsParams() *DeleteGiteaCredentialsParams { + return &DeleteGiteaCredentialsParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewDeleteGiteaCredentialsParamsWithTimeout creates a new DeleteGiteaCredentialsParams object +// with the ability to set a timeout on a request. +func NewDeleteGiteaCredentialsParamsWithTimeout(timeout time.Duration) *DeleteGiteaCredentialsParams { + return &DeleteGiteaCredentialsParams{ + timeout: timeout, + } +} + +// NewDeleteGiteaCredentialsParamsWithContext creates a new DeleteGiteaCredentialsParams object +// with the ability to set a context for a request. +func NewDeleteGiteaCredentialsParamsWithContext(ctx context.Context) *DeleteGiteaCredentialsParams { + return &DeleteGiteaCredentialsParams{ + Context: ctx, + } +} + +// NewDeleteGiteaCredentialsParamsWithHTTPClient creates a new DeleteGiteaCredentialsParams object +// with the ability to set a custom HTTPClient for a request. +func NewDeleteGiteaCredentialsParamsWithHTTPClient(client *http.Client) *DeleteGiteaCredentialsParams { + return &DeleteGiteaCredentialsParams{ + HTTPClient: client, + } +} + +/* +DeleteGiteaCredentialsParams contains all the parameters to send to the API endpoint + + for the delete gitea credentials operation. + + Typically these are written to a http.Request. +*/ +type DeleteGiteaCredentialsParams struct { + + /* ID. + + ID of the Gitea credential. + */ + ID int64 + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the delete gitea credentials params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *DeleteGiteaCredentialsParams) WithDefaults() *DeleteGiteaCredentialsParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the delete gitea credentials params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *DeleteGiteaCredentialsParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the delete gitea credentials params +func (o *DeleteGiteaCredentialsParams) WithTimeout(timeout time.Duration) *DeleteGiteaCredentialsParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the delete gitea credentials params +func (o *DeleteGiteaCredentialsParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the delete gitea credentials params +func (o *DeleteGiteaCredentialsParams) WithContext(ctx context.Context) *DeleteGiteaCredentialsParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the delete gitea credentials params +func (o *DeleteGiteaCredentialsParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the delete gitea credentials params +func (o *DeleteGiteaCredentialsParams) WithHTTPClient(client *http.Client) *DeleteGiteaCredentialsParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the delete gitea credentials params +func (o *DeleteGiteaCredentialsParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithID adds the id to the delete gitea credentials params +func (o *DeleteGiteaCredentialsParams) WithID(id int64) *DeleteGiteaCredentialsParams { + o.SetID(id) + return o +} + +// SetID adds the id to the delete gitea credentials params +func (o *DeleteGiteaCredentialsParams) SetID(id int64) { + o.ID = id +} + +// WriteToRequest writes these params to a swagger request +func (o *DeleteGiteaCredentialsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + // path param id + if err := r.SetPathParam("id", swag.FormatInt64(o.ID)); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/client/credentials/delete_gitea_credentials_responses.go b/client/credentials/delete_gitea_credentials_responses.go new file mode 100644 index 000000000..d1df7b0bc --- /dev/null +++ b/client/credentials/delete_gitea_credentials_responses.go @@ -0,0 +1,106 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package credentials + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + apiserver_params "github.com/cloudbase/garm/apiserver/params" +) + +// DeleteGiteaCredentialsReader is a Reader for the DeleteGiteaCredentials structure. +type DeleteGiteaCredentialsReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *DeleteGiteaCredentialsReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + result := NewDeleteGiteaCredentialsDefault(response.Code()) + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + if response.Code()/100 == 2 { + return result, nil + } + return nil, result +} + +// NewDeleteGiteaCredentialsDefault creates a DeleteGiteaCredentialsDefault with default headers values +func NewDeleteGiteaCredentialsDefault(code int) *DeleteGiteaCredentialsDefault { + return &DeleteGiteaCredentialsDefault{ + _statusCode: code, + } +} + +/* +DeleteGiteaCredentialsDefault describes a response with status code -1, with default header values. + +APIErrorResponse +*/ +type DeleteGiteaCredentialsDefault struct { + _statusCode int + + Payload apiserver_params.APIErrorResponse +} + +// IsSuccess returns true when this delete gitea credentials default response has a 2xx status code +func (o *DeleteGiteaCredentialsDefault) IsSuccess() bool { + return o._statusCode/100 == 2 +} + +// IsRedirect returns true when this delete gitea credentials default response has a 3xx status code +func (o *DeleteGiteaCredentialsDefault) IsRedirect() bool { + return o._statusCode/100 == 3 +} + +// IsClientError returns true when this delete gitea credentials default response has a 4xx status code +func (o *DeleteGiteaCredentialsDefault) IsClientError() bool { + return o._statusCode/100 == 4 +} + +// IsServerError returns true when this delete gitea credentials default response has a 5xx status code +func (o *DeleteGiteaCredentialsDefault) IsServerError() bool { + return o._statusCode/100 == 5 +} + +// IsCode returns true when this delete gitea credentials default response a status code equal to that given +func (o *DeleteGiteaCredentialsDefault) IsCode(code int) bool { + return o._statusCode == code +} + +// Code gets the status code for the delete gitea credentials default response +func (o *DeleteGiteaCredentialsDefault) Code() int { + return o._statusCode +} + +func (o *DeleteGiteaCredentialsDefault) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[DELETE /gitea/credentials/{id}][%d] DeleteGiteaCredentials default %s", o._statusCode, payload) +} + +func (o *DeleteGiteaCredentialsDefault) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[DELETE /gitea/credentials/{id}][%d] DeleteGiteaCredentials default %s", o._statusCode, payload) +} + +func (o *DeleteGiteaCredentialsDefault) GetPayload() apiserver_params.APIErrorResponse { + return o.Payload +} + +func (o *DeleteGiteaCredentialsDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/client/credentials/get_credentials_responses.go b/client/credentials/get_credentials_responses.go index 1c2b800b8..4538c16e3 100644 --- a/client/credentials/get_credentials_responses.go +++ b/client/credentials/get_credentials_responses.go @@ -50,10 +50,10 @@ func NewGetCredentialsOK() *GetCredentialsOK { /* GetCredentialsOK describes a response with status code 200, with default header values. -GithubCredentials +ForgeCredentials */ type GetCredentialsOK struct { - Payload garm_params.GithubCredentials + Payload garm_params.ForgeCredentials } // IsSuccess returns true when this get credentials o k response has a 2xx status code @@ -96,7 +96,7 @@ func (o *GetCredentialsOK) String() string { return fmt.Sprintf("[GET /github/credentials/{id}][%d] getCredentialsOK %s", 200, payload) } -func (o *GetCredentialsOK) GetPayload() garm_params.GithubCredentials { +func (o *GetCredentialsOK) GetPayload() garm_params.ForgeCredentials { return o.Payload } diff --git a/client/credentials/get_gitea_credentials_parameters.go b/client/credentials/get_gitea_credentials_parameters.go new file mode 100644 index 000000000..a844c326f --- /dev/null +++ b/client/credentials/get_gitea_credentials_parameters.go @@ -0,0 +1,152 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package credentials + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// NewGetGiteaCredentialsParams creates a new GetGiteaCredentialsParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewGetGiteaCredentialsParams() *GetGiteaCredentialsParams { + return &GetGiteaCredentialsParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewGetGiteaCredentialsParamsWithTimeout creates a new GetGiteaCredentialsParams object +// with the ability to set a timeout on a request. +func NewGetGiteaCredentialsParamsWithTimeout(timeout time.Duration) *GetGiteaCredentialsParams { + return &GetGiteaCredentialsParams{ + timeout: timeout, + } +} + +// NewGetGiteaCredentialsParamsWithContext creates a new GetGiteaCredentialsParams object +// with the ability to set a context for a request. +func NewGetGiteaCredentialsParamsWithContext(ctx context.Context) *GetGiteaCredentialsParams { + return &GetGiteaCredentialsParams{ + Context: ctx, + } +} + +// NewGetGiteaCredentialsParamsWithHTTPClient creates a new GetGiteaCredentialsParams object +// with the ability to set a custom HTTPClient for a request. +func NewGetGiteaCredentialsParamsWithHTTPClient(client *http.Client) *GetGiteaCredentialsParams { + return &GetGiteaCredentialsParams{ + HTTPClient: client, + } +} + +/* +GetGiteaCredentialsParams contains all the parameters to send to the API endpoint + + for the get gitea credentials operation. + + Typically these are written to a http.Request. +*/ +type GetGiteaCredentialsParams struct { + + /* ID. + + ID of the Gitea credential. + */ + ID int64 + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the get gitea credentials params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *GetGiteaCredentialsParams) WithDefaults() *GetGiteaCredentialsParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the get gitea credentials params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *GetGiteaCredentialsParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the get gitea credentials params +func (o *GetGiteaCredentialsParams) WithTimeout(timeout time.Duration) *GetGiteaCredentialsParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the get gitea credentials params +func (o *GetGiteaCredentialsParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the get gitea credentials params +func (o *GetGiteaCredentialsParams) WithContext(ctx context.Context) *GetGiteaCredentialsParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the get gitea credentials params +func (o *GetGiteaCredentialsParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the get gitea credentials params +func (o *GetGiteaCredentialsParams) WithHTTPClient(client *http.Client) *GetGiteaCredentialsParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the get gitea credentials params +func (o *GetGiteaCredentialsParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithID adds the id to the get gitea credentials params +func (o *GetGiteaCredentialsParams) WithID(id int64) *GetGiteaCredentialsParams { + o.SetID(id) + return o +} + +// SetID adds the id to the get gitea credentials params +func (o *GetGiteaCredentialsParams) SetID(id int64) { + o.ID = id +} + +// WriteToRequest writes these params to a swagger request +func (o *GetGiteaCredentialsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + // path param id + if err := r.SetPathParam("id", swag.FormatInt64(o.ID)); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/client/credentials/get_gitea_credentials_responses.go b/client/credentials/get_gitea_credentials_responses.go new file mode 100644 index 000000000..ba116d632 --- /dev/null +++ b/client/credentials/get_gitea_credentials_responses.go @@ -0,0 +1,179 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package credentials + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + apiserver_params "github.com/cloudbase/garm/apiserver/params" + garm_params "github.com/cloudbase/garm/params" +) + +// GetGiteaCredentialsReader is a Reader for the GetGiteaCredentials structure. +type GetGiteaCredentialsReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *GetGiteaCredentialsReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewGetGiteaCredentialsOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 400: + result := NewGetGiteaCredentialsBadRequest() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + default: + return nil, runtime.NewAPIError("[GET /gitea/credentials/{id}] GetGiteaCredentials", response, response.Code()) + } +} + +// NewGetGiteaCredentialsOK creates a GetGiteaCredentialsOK with default headers values +func NewGetGiteaCredentialsOK() *GetGiteaCredentialsOK { + return &GetGiteaCredentialsOK{} +} + +/* +GetGiteaCredentialsOK describes a response with status code 200, with default header values. + +ForgeCredentials +*/ +type GetGiteaCredentialsOK struct { + Payload garm_params.ForgeCredentials +} + +// IsSuccess returns true when this get gitea credentials o k response has a 2xx status code +func (o *GetGiteaCredentialsOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this get gitea credentials o k response has a 3xx status code +func (o *GetGiteaCredentialsOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this get gitea credentials o k response has a 4xx status code +func (o *GetGiteaCredentialsOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this get gitea credentials o k response has a 5xx status code +func (o *GetGiteaCredentialsOK) IsServerError() bool { + return false +} + +// IsCode returns true when this get gitea credentials o k response a status code equal to that given +func (o *GetGiteaCredentialsOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the get gitea credentials o k response +func (o *GetGiteaCredentialsOK) Code() int { + return 200 +} + +func (o *GetGiteaCredentialsOK) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /gitea/credentials/{id}][%d] getGiteaCredentialsOK %s", 200, payload) +} + +func (o *GetGiteaCredentialsOK) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /gitea/credentials/{id}][%d] getGiteaCredentialsOK %s", 200, payload) +} + +func (o *GetGiteaCredentialsOK) GetPayload() garm_params.ForgeCredentials { + return o.Payload +} + +func (o *GetGiteaCredentialsOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewGetGiteaCredentialsBadRequest creates a GetGiteaCredentialsBadRequest with default headers values +func NewGetGiteaCredentialsBadRequest() *GetGiteaCredentialsBadRequest { + return &GetGiteaCredentialsBadRequest{} +} + +/* +GetGiteaCredentialsBadRequest describes a response with status code 400, with default header values. + +APIErrorResponse +*/ +type GetGiteaCredentialsBadRequest struct { + Payload apiserver_params.APIErrorResponse +} + +// IsSuccess returns true when this get gitea credentials bad request response has a 2xx status code +func (o *GetGiteaCredentialsBadRequest) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this get gitea credentials bad request response has a 3xx status code +func (o *GetGiteaCredentialsBadRequest) IsRedirect() bool { + return false +} + +// IsClientError returns true when this get gitea credentials bad request response has a 4xx status code +func (o *GetGiteaCredentialsBadRequest) IsClientError() bool { + return true +} + +// IsServerError returns true when this get gitea credentials bad request response has a 5xx status code +func (o *GetGiteaCredentialsBadRequest) IsServerError() bool { + return false +} + +// IsCode returns true when this get gitea credentials bad request response a status code equal to that given +func (o *GetGiteaCredentialsBadRequest) IsCode(code int) bool { + return code == 400 +} + +// Code gets the status code for the get gitea credentials bad request response +func (o *GetGiteaCredentialsBadRequest) Code() int { + return 400 +} + +func (o *GetGiteaCredentialsBadRequest) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /gitea/credentials/{id}][%d] getGiteaCredentialsBadRequest %s", 400, payload) +} + +func (o *GetGiteaCredentialsBadRequest) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /gitea/credentials/{id}][%d] getGiteaCredentialsBadRequest %s", 400, payload) +} + +func (o *GetGiteaCredentialsBadRequest) GetPayload() apiserver_params.APIErrorResponse { + return o.Payload +} + +func (o *GetGiteaCredentialsBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/client/credentials/list_gitea_credentials_parameters.go b/client/credentials/list_gitea_credentials_parameters.go new file mode 100644 index 000000000..5e321a88c --- /dev/null +++ b/client/credentials/list_gitea_credentials_parameters.go @@ -0,0 +1,128 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package credentials + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" +) + +// NewListGiteaCredentialsParams creates a new ListGiteaCredentialsParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewListGiteaCredentialsParams() *ListGiteaCredentialsParams { + return &ListGiteaCredentialsParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewListGiteaCredentialsParamsWithTimeout creates a new ListGiteaCredentialsParams object +// with the ability to set a timeout on a request. +func NewListGiteaCredentialsParamsWithTimeout(timeout time.Duration) *ListGiteaCredentialsParams { + return &ListGiteaCredentialsParams{ + timeout: timeout, + } +} + +// NewListGiteaCredentialsParamsWithContext creates a new ListGiteaCredentialsParams object +// with the ability to set a context for a request. +func NewListGiteaCredentialsParamsWithContext(ctx context.Context) *ListGiteaCredentialsParams { + return &ListGiteaCredentialsParams{ + Context: ctx, + } +} + +// NewListGiteaCredentialsParamsWithHTTPClient creates a new ListGiteaCredentialsParams object +// with the ability to set a custom HTTPClient for a request. +func NewListGiteaCredentialsParamsWithHTTPClient(client *http.Client) *ListGiteaCredentialsParams { + return &ListGiteaCredentialsParams{ + HTTPClient: client, + } +} + +/* +ListGiteaCredentialsParams contains all the parameters to send to the API endpoint + + for the list gitea credentials operation. + + Typically these are written to a http.Request. +*/ +type ListGiteaCredentialsParams struct { + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the list gitea credentials params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *ListGiteaCredentialsParams) WithDefaults() *ListGiteaCredentialsParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the list gitea credentials params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *ListGiteaCredentialsParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the list gitea credentials params +func (o *ListGiteaCredentialsParams) WithTimeout(timeout time.Duration) *ListGiteaCredentialsParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the list gitea credentials params +func (o *ListGiteaCredentialsParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the list gitea credentials params +func (o *ListGiteaCredentialsParams) WithContext(ctx context.Context) *ListGiteaCredentialsParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the list gitea credentials params +func (o *ListGiteaCredentialsParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the list gitea credentials params +func (o *ListGiteaCredentialsParams) WithHTTPClient(client *http.Client) *ListGiteaCredentialsParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the list gitea credentials params +func (o *ListGiteaCredentialsParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WriteToRequest writes these params to a swagger request +func (o *ListGiteaCredentialsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/client/credentials/list_gitea_credentials_responses.go b/client/credentials/list_gitea_credentials_responses.go new file mode 100644 index 000000000..f27864be2 --- /dev/null +++ b/client/credentials/list_gitea_credentials_responses.go @@ -0,0 +1,179 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package credentials + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + apiserver_params "github.com/cloudbase/garm/apiserver/params" + garm_params "github.com/cloudbase/garm/params" +) + +// ListGiteaCredentialsReader is a Reader for the ListGiteaCredentials structure. +type ListGiteaCredentialsReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *ListGiteaCredentialsReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewListGiteaCredentialsOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 400: + result := NewListGiteaCredentialsBadRequest() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + default: + return nil, runtime.NewAPIError("[GET /gitea/credentials] ListGiteaCredentials", response, response.Code()) + } +} + +// NewListGiteaCredentialsOK creates a ListGiteaCredentialsOK with default headers values +func NewListGiteaCredentialsOK() *ListGiteaCredentialsOK { + return &ListGiteaCredentialsOK{} +} + +/* +ListGiteaCredentialsOK describes a response with status code 200, with default header values. + +Credentials +*/ +type ListGiteaCredentialsOK struct { + Payload garm_params.Credentials +} + +// IsSuccess returns true when this list gitea credentials o k response has a 2xx status code +func (o *ListGiteaCredentialsOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this list gitea credentials o k response has a 3xx status code +func (o *ListGiteaCredentialsOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this list gitea credentials o k response has a 4xx status code +func (o *ListGiteaCredentialsOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this list gitea credentials o k response has a 5xx status code +func (o *ListGiteaCredentialsOK) IsServerError() bool { + return false +} + +// IsCode returns true when this list gitea credentials o k response a status code equal to that given +func (o *ListGiteaCredentialsOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the list gitea credentials o k response +func (o *ListGiteaCredentialsOK) Code() int { + return 200 +} + +func (o *ListGiteaCredentialsOK) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /gitea/credentials][%d] listGiteaCredentialsOK %s", 200, payload) +} + +func (o *ListGiteaCredentialsOK) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /gitea/credentials][%d] listGiteaCredentialsOK %s", 200, payload) +} + +func (o *ListGiteaCredentialsOK) GetPayload() garm_params.Credentials { + return o.Payload +} + +func (o *ListGiteaCredentialsOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewListGiteaCredentialsBadRequest creates a ListGiteaCredentialsBadRequest with default headers values +func NewListGiteaCredentialsBadRequest() *ListGiteaCredentialsBadRequest { + return &ListGiteaCredentialsBadRequest{} +} + +/* +ListGiteaCredentialsBadRequest describes a response with status code 400, with default header values. + +APIErrorResponse +*/ +type ListGiteaCredentialsBadRequest struct { + Payload apiserver_params.APIErrorResponse +} + +// IsSuccess returns true when this list gitea credentials bad request response has a 2xx status code +func (o *ListGiteaCredentialsBadRequest) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this list gitea credentials bad request response has a 3xx status code +func (o *ListGiteaCredentialsBadRequest) IsRedirect() bool { + return false +} + +// IsClientError returns true when this list gitea credentials bad request response has a 4xx status code +func (o *ListGiteaCredentialsBadRequest) IsClientError() bool { + return true +} + +// IsServerError returns true when this list gitea credentials bad request response has a 5xx status code +func (o *ListGiteaCredentialsBadRequest) IsServerError() bool { + return false +} + +// IsCode returns true when this list gitea credentials bad request response a status code equal to that given +func (o *ListGiteaCredentialsBadRequest) IsCode(code int) bool { + return code == 400 +} + +// Code gets the status code for the list gitea credentials bad request response +func (o *ListGiteaCredentialsBadRequest) Code() int { + return 400 +} + +func (o *ListGiteaCredentialsBadRequest) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /gitea/credentials][%d] listGiteaCredentialsBadRequest %s", 400, payload) +} + +func (o *ListGiteaCredentialsBadRequest) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /gitea/credentials][%d] listGiteaCredentialsBadRequest %s", 400, payload) +} + +func (o *ListGiteaCredentialsBadRequest) GetPayload() apiserver_params.APIErrorResponse { + return o.Payload +} + +func (o *ListGiteaCredentialsBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/client/credentials/update_credentials_responses.go b/client/credentials/update_credentials_responses.go index b02546045..6a9f37f8d 100644 --- a/client/credentials/update_credentials_responses.go +++ b/client/credentials/update_credentials_responses.go @@ -50,10 +50,10 @@ func NewUpdateCredentialsOK() *UpdateCredentialsOK { /* UpdateCredentialsOK describes a response with status code 200, with default header values. -GithubCredentials +ForgeCredentials */ type UpdateCredentialsOK struct { - Payload garm_params.GithubCredentials + Payload garm_params.ForgeCredentials } // IsSuccess returns true when this update credentials o k response has a 2xx status code @@ -96,7 +96,7 @@ func (o *UpdateCredentialsOK) String() string { return fmt.Sprintf("[PUT /github/credentials/{id}][%d] updateCredentialsOK %s", 200, payload) } -func (o *UpdateCredentialsOK) GetPayload() garm_params.GithubCredentials { +func (o *UpdateCredentialsOK) GetPayload() garm_params.ForgeCredentials { return o.Payload } diff --git a/client/credentials/update_gitea_credentials_parameters.go b/client/credentials/update_gitea_credentials_parameters.go new file mode 100644 index 000000000..1907a0f2f --- /dev/null +++ b/client/credentials/update_gitea_credentials_parameters.go @@ -0,0 +1,174 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package credentials + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + + garm_params "github.com/cloudbase/garm/params" +) + +// NewUpdateGiteaCredentialsParams creates a new UpdateGiteaCredentialsParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewUpdateGiteaCredentialsParams() *UpdateGiteaCredentialsParams { + return &UpdateGiteaCredentialsParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewUpdateGiteaCredentialsParamsWithTimeout creates a new UpdateGiteaCredentialsParams object +// with the ability to set a timeout on a request. +func NewUpdateGiteaCredentialsParamsWithTimeout(timeout time.Duration) *UpdateGiteaCredentialsParams { + return &UpdateGiteaCredentialsParams{ + timeout: timeout, + } +} + +// NewUpdateGiteaCredentialsParamsWithContext creates a new UpdateGiteaCredentialsParams object +// with the ability to set a context for a request. +func NewUpdateGiteaCredentialsParamsWithContext(ctx context.Context) *UpdateGiteaCredentialsParams { + return &UpdateGiteaCredentialsParams{ + Context: ctx, + } +} + +// NewUpdateGiteaCredentialsParamsWithHTTPClient creates a new UpdateGiteaCredentialsParams object +// with the ability to set a custom HTTPClient for a request. +func NewUpdateGiteaCredentialsParamsWithHTTPClient(client *http.Client) *UpdateGiteaCredentialsParams { + return &UpdateGiteaCredentialsParams{ + HTTPClient: client, + } +} + +/* +UpdateGiteaCredentialsParams contains all the parameters to send to the API endpoint + + for the update gitea credentials operation. + + Typically these are written to a http.Request. +*/ +type UpdateGiteaCredentialsParams struct { + + /* Body. + + Parameters used when updating a Gitea credential. + */ + Body garm_params.UpdateGiteaCredentialsParams + + /* ID. + + ID of the Gitea credential. + */ + ID int64 + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the update gitea credentials params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *UpdateGiteaCredentialsParams) WithDefaults() *UpdateGiteaCredentialsParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the update gitea credentials params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *UpdateGiteaCredentialsParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the update gitea credentials params +func (o *UpdateGiteaCredentialsParams) WithTimeout(timeout time.Duration) *UpdateGiteaCredentialsParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the update gitea credentials params +func (o *UpdateGiteaCredentialsParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the update gitea credentials params +func (o *UpdateGiteaCredentialsParams) WithContext(ctx context.Context) *UpdateGiteaCredentialsParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the update gitea credentials params +func (o *UpdateGiteaCredentialsParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the update gitea credentials params +func (o *UpdateGiteaCredentialsParams) WithHTTPClient(client *http.Client) *UpdateGiteaCredentialsParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the update gitea credentials params +func (o *UpdateGiteaCredentialsParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithBody adds the body to the update gitea credentials params +func (o *UpdateGiteaCredentialsParams) WithBody(body garm_params.UpdateGiteaCredentialsParams) *UpdateGiteaCredentialsParams { + o.SetBody(body) + return o +} + +// SetBody adds the body to the update gitea credentials params +func (o *UpdateGiteaCredentialsParams) SetBody(body garm_params.UpdateGiteaCredentialsParams) { + o.Body = body +} + +// WithID adds the id to the update gitea credentials params +func (o *UpdateGiteaCredentialsParams) WithID(id int64) *UpdateGiteaCredentialsParams { + o.SetID(id) + return o +} + +// SetID adds the id to the update gitea credentials params +func (o *UpdateGiteaCredentialsParams) SetID(id int64) { + o.ID = id +} + +// WriteToRequest writes these params to a swagger request +func (o *UpdateGiteaCredentialsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + if err := r.SetBodyParam(o.Body); err != nil { + return err + } + + // path param id + if err := r.SetPathParam("id", swag.FormatInt64(o.ID)); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/client/credentials/update_gitea_credentials_responses.go b/client/credentials/update_gitea_credentials_responses.go new file mode 100644 index 000000000..edbb54d8b --- /dev/null +++ b/client/credentials/update_gitea_credentials_responses.go @@ -0,0 +1,179 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package credentials + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + apiserver_params "github.com/cloudbase/garm/apiserver/params" + garm_params "github.com/cloudbase/garm/params" +) + +// UpdateGiteaCredentialsReader is a Reader for the UpdateGiteaCredentials structure. +type UpdateGiteaCredentialsReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *UpdateGiteaCredentialsReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewUpdateGiteaCredentialsOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 400: + result := NewUpdateGiteaCredentialsBadRequest() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + default: + return nil, runtime.NewAPIError("[PUT /gitea/credentials/{id}] UpdateGiteaCredentials", response, response.Code()) + } +} + +// NewUpdateGiteaCredentialsOK creates a UpdateGiteaCredentialsOK with default headers values +func NewUpdateGiteaCredentialsOK() *UpdateGiteaCredentialsOK { + return &UpdateGiteaCredentialsOK{} +} + +/* +UpdateGiteaCredentialsOK describes a response with status code 200, with default header values. + +ForgeCredentials +*/ +type UpdateGiteaCredentialsOK struct { + Payload garm_params.ForgeCredentials +} + +// IsSuccess returns true when this update gitea credentials o k response has a 2xx status code +func (o *UpdateGiteaCredentialsOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this update gitea credentials o k response has a 3xx status code +func (o *UpdateGiteaCredentialsOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this update gitea credentials o k response has a 4xx status code +func (o *UpdateGiteaCredentialsOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this update gitea credentials o k response has a 5xx status code +func (o *UpdateGiteaCredentialsOK) IsServerError() bool { + return false +} + +// IsCode returns true when this update gitea credentials o k response a status code equal to that given +func (o *UpdateGiteaCredentialsOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the update gitea credentials o k response +func (o *UpdateGiteaCredentialsOK) Code() int { + return 200 +} + +func (o *UpdateGiteaCredentialsOK) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PUT /gitea/credentials/{id}][%d] updateGiteaCredentialsOK %s", 200, payload) +} + +func (o *UpdateGiteaCredentialsOK) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PUT /gitea/credentials/{id}][%d] updateGiteaCredentialsOK %s", 200, payload) +} + +func (o *UpdateGiteaCredentialsOK) GetPayload() garm_params.ForgeCredentials { + return o.Payload +} + +func (o *UpdateGiteaCredentialsOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewUpdateGiteaCredentialsBadRequest creates a UpdateGiteaCredentialsBadRequest with default headers values +func NewUpdateGiteaCredentialsBadRequest() *UpdateGiteaCredentialsBadRequest { + return &UpdateGiteaCredentialsBadRequest{} +} + +/* +UpdateGiteaCredentialsBadRequest describes a response with status code 400, with default header values. + +APIErrorResponse +*/ +type UpdateGiteaCredentialsBadRequest struct { + Payload apiserver_params.APIErrorResponse +} + +// IsSuccess returns true when this update gitea credentials bad request response has a 2xx status code +func (o *UpdateGiteaCredentialsBadRequest) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this update gitea credentials bad request response has a 3xx status code +func (o *UpdateGiteaCredentialsBadRequest) IsRedirect() bool { + return false +} + +// IsClientError returns true when this update gitea credentials bad request response has a 4xx status code +func (o *UpdateGiteaCredentialsBadRequest) IsClientError() bool { + return true +} + +// IsServerError returns true when this update gitea credentials bad request response has a 5xx status code +func (o *UpdateGiteaCredentialsBadRequest) IsServerError() bool { + return false +} + +// IsCode returns true when this update gitea credentials bad request response a status code equal to that given +func (o *UpdateGiteaCredentialsBadRequest) IsCode(code int) bool { + return code == 400 +} + +// Code gets the status code for the update gitea credentials bad request response +func (o *UpdateGiteaCredentialsBadRequest) Code() int { + return 400 +} + +func (o *UpdateGiteaCredentialsBadRequest) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PUT /gitea/credentials/{id}][%d] updateGiteaCredentialsBadRequest %s", 400, payload) +} + +func (o *UpdateGiteaCredentialsBadRequest) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PUT /gitea/credentials/{id}][%d] updateGiteaCredentialsBadRequest %s", 400, payload) +} + +func (o *UpdateGiteaCredentialsBadRequest) GetPayload() apiserver_params.APIErrorResponse { + return o.Payload +} + +func (o *UpdateGiteaCredentialsBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/client/endpoints/create_gitea_endpoint_parameters.go b/client/endpoints/create_gitea_endpoint_parameters.go new file mode 100644 index 000000000..11dfa73f8 --- /dev/null +++ b/client/endpoints/create_gitea_endpoint_parameters.go @@ -0,0 +1,151 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package endpoints + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + + garm_params "github.com/cloudbase/garm/params" +) + +// NewCreateGiteaEndpointParams creates a new CreateGiteaEndpointParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewCreateGiteaEndpointParams() *CreateGiteaEndpointParams { + return &CreateGiteaEndpointParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewCreateGiteaEndpointParamsWithTimeout creates a new CreateGiteaEndpointParams object +// with the ability to set a timeout on a request. +func NewCreateGiteaEndpointParamsWithTimeout(timeout time.Duration) *CreateGiteaEndpointParams { + return &CreateGiteaEndpointParams{ + timeout: timeout, + } +} + +// NewCreateGiteaEndpointParamsWithContext creates a new CreateGiteaEndpointParams object +// with the ability to set a context for a request. +func NewCreateGiteaEndpointParamsWithContext(ctx context.Context) *CreateGiteaEndpointParams { + return &CreateGiteaEndpointParams{ + Context: ctx, + } +} + +// NewCreateGiteaEndpointParamsWithHTTPClient creates a new CreateGiteaEndpointParams object +// with the ability to set a custom HTTPClient for a request. +func NewCreateGiteaEndpointParamsWithHTTPClient(client *http.Client) *CreateGiteaEndpointParams { + return &CreateGiteaEndpointParams{ + HTTPClient: client, + } +} + +/* +CreateGiteaEndpointParams contains all the parameters to send to the API endpoint + + for the create gitea endpoint operation. + + Typically these are written to a http.Request. +*/ +type CreateGiteaEndpointParams struct { + + /* Body. + + Parameters used when creating a Gitea endpoint. + */ + Body garm_params.CreateGiteaEndpointParams + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the create gitea endpoint params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *CreateGiteaEndpointParams) WithDefaults() *CreateGiteaEndpointParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the create gitea endpoint params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *CreateGiteaEndpointParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the create gitea endpoint params +func (o *CreateGiteaEndpointParams) WithTimeout(timeout time.Duration) *CreateGiteaEndpointParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the create gitea endpoint params +func (o *CreateGiteaEndpointParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the create gitea endpoint params +func (o *CreateGiteaEndpointParams) WithContext(ctx context.Context) *CreateGiteaEndpointParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the create gitea endpoint params +func (o *CreateGiteaEndpointParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the create gitea endpoint params +func (o *CreateGiteaEndpointParams) WithHTTPClient(client *http.Client) *CreateGiteaEndpointParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the create gitea endpoint params +func (o *CreateGiteaEndpointParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithBody adds the body to the create gitea endpoint params +func (o *CreateGiteaEndpointParams) WithBody(body garm_params.CreateGiteaEndpointParams) *CreateGiteaEndpointParams { + o.SetBody(body) + return o +} + +// SetBody adds the body to the create gitea endpoint params +func (o *CreateGiteaEndpointParams) SetBody(body garm_params.CreateGiteaEndpointParams) { + o.Body = body +} + +// WriteToRequest writes these params to a swagger request +func (o *CreateGiteaEndpointParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + if err := r.SetBodyParam(o.Body); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/client/endpoints/create_gitea_endpoint_responses.go b/client/endpoints/create_gitea_endpoint_responses.go new file mode 100644 index 000000000..6e99a973f --- /dev/null +++ b/client/endpoints/create_gitea_endpoint_responses.go @@ -0,0 +1,184 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package endpoints + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + apiserver_params "github.com/cloudbase/garm/apiserver/params" + garm_params "github.com/cloudbase/garm/params" +) + +// CreateGiteaEndpointReader is a Reader for the CreateGiteaEndpoint structure. +type CreateGiteaEndpointReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *CreateGiteaEndpointReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewCreateGiteaEndpointOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + default: + result := NewCreateGiteaEndpointDefault(response.Code()) + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + if response.Code()/100 == 2 { + return result, nil + } + return nil, result + } +} + +// NewCreateGiteaEndpointOK creates a CreateGiteaEndpointOK with default headers values +func NewCreateGiteaEndpointOK() *CreateGiteaEndpointOK { + return &CreateGiteaEndpointOK{} +} + +/* +CreateGiteaEndpointOK describes a response with status code 200, with default header values. + +ForgeEndpoint +*/ +type CreateGiteaEndpointOK struct { + Payload garm_params.ForgeEndpoint +} + +// IsSuccess returns true when this create gitea endpoint o k response has a 2xx status code +func (o *CreateGiteaEndpointOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this create gitea endpoint o k response has a 3xx status code +func (o *CreateGiteaEndpointOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this create gitea endpoint o k response has a 4xx status code +func (o *CreateGiteaEndpointOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this create gitea endpoint o k response has a 5xx status code +func (o *CreateGiteaEndpointOK) IsServerError() bool { + return false +} + +// IsCode returns true when this create gitea endpoint o k response a status code equal to that given +func (o *CreateGiteaEndpointOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the create gitea endpoint o k response +func (o *CreateGiteaEndpointOK) Code() int { + return 200 +} + +func (o *CreateGiteaEndpointOK) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /gitea/endpoints][%d] createGiteaEndpointOK %s", 200, payload) +} + +func (o *CreateGiteaEndpointOK) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /gitea/endpoints][%d] createGiteaEndpointOK %s", 200, payload) +} + +func (o *CreateGiteaEndpointOK) GetPayload() garm_params.ForgeEndpoint { + return o.Payload +} + +func (o *CreateGiteaEndpointOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewCreateGiteaEndpointDefault creates a CreateGiteaEndpointDefault with default headers values +func NewCreateGiteaEndpointDefault(code int) *CreateGiteaEndpointDefault { + return &CreateGiteaEndpointDefault{ + _statusCode: code, + } +} + +/* +CreateGiteaEndpointDefault describes a response with status code -1, with default header values. + +APIErrorResponse +*/ +type CreateGiteaEndpointDefault struct { + _statusCode int + + Payload apiserver_params.APIErrorResponse +} + +// IsSuccess returns true when this create gitea endpoint default response has a 2xx status code +func (o *CreateGiteaEndpointDefault) IsSuccess() bool { + return o._statusCode/100 == 2 +} + +// IsRedirect returns true when this create gitea endpoint default response has a 3xx status code +func (o *CreateGiteaEndpointDefault) IsRedirect() bool { + return o._statusCode/100 == 3 +} + +// IsClientError returns true when this create gitea endpoint default response has a 4xx status code +func (o *CreateGiteaEndpointDefault) IsClientError() bool { + return o._statusCode/100 == 4 +} + +// IsServerError returns true when this create gitea endpoint default response has a 5xx status code +func (o *CreateGiteaEndpointDefault) IsServerError() bool { + return o._statusCode/100 == 5 +} + +// IsCode returns true when this create gitea endpoint default response a status code equal to that given +func (o *CreateGiteaEndpointDefault) IsCode(code int) bool { + return o._statusCode == code +} + +// Code gets the status code for the create gitea endpoint default response +func (o *CreateGiteaEndpointDefault) Code() int { + return o._statusCode +} + +func (o *CreateGiteaEndpointDefault) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /gitea/endpoints][%d] CreateGiteaEndpoint default %s", o._statusCode, payload) +} + +func (o *CreateGiteaEndpointDefault) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /gitea/endpoints][%d] CreateGiteaEndpoint default %s", o._statusCode, payload) +} + +func (o *CreateGiteaEndpointDefault) GetPayload() apiserver_params.APIErrorResponse { + return o.Payload +} + +func (o *CreateGiteaEndpointDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/client/endpoints/create_github_endpoint_responses.go b/client/endpoints/create_github_endpoint_responses.go index acd950884..60961f3a4 100644 --- a/client/endpoints/create_github_endpoint_responses.go +++ b/client/endpoints/create_github_endpoint_responses.go @@ -51,10 +51,10 @@ func NewCreateGithubEndpointOK() *CreateGithubEndpointOK { /* CreateGithubEndpointOK describes a response with status code 200, with default header values. -GithubEndpoint +ForgeEndpoint */ type CreateGithubEndpointOK struct { - Payload garm_params.GithubEndpoint + Payload garm_params.ForgeEndpoint } // IsSuccess returns true when this create github endpoint o k response has a 2xx status code @@ -97,7 +97,7 @@ func (o *CreateGithubEndpointOK) String() string { return fmt.Sprintf("[POST /github/endpoints][%d] createGithubEndpointOK %s", 200, payload) } -func (o *CreateGithubEndpointOK) GetPayload() garm_params.GithubEndpoint { +func (o *CreateGithubEndpointOK) GetPayload() garm_params.ForgeEndpoint { return o.Payload } diff --git a/client/endpoints/delete_gitea_endpoint_parameters.go b/client/endpoints/delete_gitea_endpoint_parameters.go new file mode 100644 index 000000000..f7ea5a5d5 --- /dev/null +++ b/client/endpoints/delete_gitea_endpoint_parameters.go @@ -0,0 +1,151 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package endpoints + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" +) + +// NewDeleteGiteaEndpointParams creates a new DeleteGiteaEndpointParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewDeleteGiteaEndpointParams() *DeleteGiteaEndpointParams { + return &DeleteGiteaEndpointParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewDeleteGiteaEndpointParamsWithTimeout creates a new DeleteGiteaEndpointParams object +// with the ability to set a timeout on a request. +func NewDeleteGiteaEndpointParamsWithTimeout(timeout time.Duration) *DeleteGiteaEndpointParams { + return &DeleteGiteaEndpointParams{ + timeout: timeout, + } +} + +// NewDeleteGiteaEndpointParamsWithContext creates a new DeleteGiteaEndpointParams object +// with the ability to set a context for a request. +func NewDeleteGiteaEndpointParamsWithContext(ctx context.Context) *DeleteGiteaEndpointParams { + return &DeleteGiteaEndpointParams{ + Context: ctx, + } +} + +// NewDeleteGiteaEndpointParamsWithHTTPClient creates a new DeleteGiteaEndpointParams object +// with the ability to set a custom HTTPClient for a request. +func NewDeleteGiteaEndpointParamsWithHTTPClient(client *http.Client) *DeleteGiteaEndpointParams { + return &DeleteGiteaEndpointParams{ + HTTPClient: client, + } +} + +/* +DeleteGiteaEndpointParams contains all the parameters to send to the API endpoint + + for the delete gitea endpoint operation. + + Typically these are written to a http.Request. +*/ +type DeleteGiteaEndpointParams struct { + + /* Name. + + The name of the Gitea endpoint. + */ + Name string + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the delete gitea endpoint params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *DeleteGiteaEndpointParams) WithDefaults() *DeleteGiteaEndpointParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the delete gitea endpoint params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *DeleteGiteaEndpointParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the delete gitea endpoint params +func (o *DeleteGiteaEndpointParams) WithTimeout(timeout time.Duration) *DeleteGiteaEndpointParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the delete gitea endpoint params +func (o *DeleteGiteaEndpointParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the delete gitea endpoint params +func (o *DeleteGiteaEndpointParams) WithContext(ctx context.Context) *DeleteGiteaEndpointParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the delete gitea endpoint params +func (o *DeleteGiteaEndpointParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the delete gitea endpoint params +func (o *DeleteGiteaEndpointParams) WithHTTPClient(client *http.Client) *DeleteGiteaEndpointParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the delete gitea endpoint params +func (o *DeleteGiteaEndpointParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithName adds the name to the delete gitea endpoint params +func (o *DeleteGiteaEndpointParams) WithName(name string) *DeleteGiteaEndpointParams { + o.SetName(name) + return o +} + +// SetName adds the name to the delete gitea endpoint params +func (o *DeleteGiteaEndpointParams) SetName(name string) { + o.Name = name +} + +// WriteToRequest writes these params to a swagger request +func (o *DeleteGiteaEndpointParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + // path param name + if err := r.SetPathParam("name", o.Name); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/client/endpoints/delete_gitea_endpoint_responses.go b/client/endpoints/delete_gitea_endpoint_responses.go new file mode 100644 index 000000000..787d6585e --- /dev/null +++ b/client/endpoints/delete_gitea_endpoint_responses.go @@ -0,0 +1,106 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package endpoints + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + apiserver_params "github.com/cloudbase/garm/apiserver/params" +) + +// DeleteGiteaEndpointReader is a Reader for the DeleteGiteaEndpoint structure. +type DeleteGiteaEndpointReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *DeleteGiteaEndpointReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + result := NewDeleteGiteaEndpointDefault(response.Code()) + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + if response.Code()/100 == 2 { + return result, nil + } + return nil, result +} + +// NewDeleteGiteaEndpointDefault creates a DeleteGiteaEndpointDefault with default headers values +func NewDeleteGiteaEndpointDefault(code int) *DeleteGiteaEndpointDefault { + return &DeleteGiteaEndpointDefault{ + _statusCode: code, + } +} + +/* +DeleteGiteaEndpointDefault describes a response with status code -1, with default header values. + +APIErrorResponse +*/ +type DeleteGiteaEndpointDefault struct { + _statusCode int + + Payload apiserver_params.APIErrorResponse +} + +// IsSuccess returns true when this delete gitea endpoint default response has a 2xx status code +func (o *DeleteGiteaEndpointDefault) IsSuccess() bool { + return o._statusCode/100 == 2 +} + +// IsRedirect returns true when this delete gitea endpoint default response has a 3xx status code +func (o *DeleteGiteaEndpointDefault) IsRedirect() bool { + return o._statusCode/100 == 3 +} + +// IsClientError returns true when this delete gitea endpoint default response has a 4xx status code +func (o *DeleteGiteaEndpointDefault) IsClientError() bool { + return o._statusCode/100 == 4 +} + +// IsServerError returns true when this delete gitea endpoint default response has a 5xx status code +func (o *DeleteGiteaEndpointDefault) IsServerError() bool { + return o._statusCode/100 == 5 +} + +// IsCode returns true when this delete gitea endpoint default response a status code equal to that given +func (o *DeleteGiteaEndpointDefault) IsCode(code int) bool { + return o._statusCode == code +} + +// Code gets the status code for the delete gitea endpoint default response +func (o *DeleteGiteaEndpointDefault) Code() int { + return o._statusCode +} + +func (o *DeleteGiteaEndpointDefault) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[DELETE /gitea/endpoints/{name}][%d] DeleteGiteaEndpoint default %s", o._statusCode, payload) +} + +func (o *DeleteGiteaEndpointDefault) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[DELETE /gitea/endpoints/{name}][%d] DeleteGiteaEndpoint default %s", o._statusCode, payload) +} + +func (o *DeleteGiteaEndpointDefault) GetPayload() apiserver_params.APIErrorResponse { + return o.Payload +} + +func (o *DeleteGiteaEndpointDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/client/endpoints/endpoints_client.go b/client/endpoints/endpoints_client.go index 9b951b2c9..740195778 100644 --- a/client/endpoints/endpoints_client.go +++ b/client/endpoints/endpoints_client.go @@ -54,19 +54,67 @@ type ClientOption func(*runtime.ClientOperation) // ClientService is the interface for Client methods type ClientService interface { + CreateGiteaEndpoint(params *CreateGiteaEndpointParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*CreateGiteaEndpointOK, error) + CreateGithubEndpoint(params *CreateGithubEndpointParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*CreateGithubEndpointOK, error) + DeleteGiteaEndpoint(params *DeleteGiteaEndpointParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) error + DeleteGithubEndpoint(params *DeleteGithubEndpointParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) error + GetGiteaEndpoint(params *GetGiteaEndpointParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetGiteaEndpointOK, error) + GetGithubEndpoint(params *GetGithubEndpointParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetGithubEndpointOK, error) + ListGiteaEndpoints(params *ListGiteaEndpointsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*ListGiteaEndpointsOK, error) + ListGithubEndpoints(params *ListGithubEndpointsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*ListGithubEndpointsOK, error) + UpdateGiteaEndpoint(params *UpdateGiteaEndpointParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UpdateGiteaEndpointOK, error) + UpdateGithubEndpoint(params *UpdateGithubEndpointParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UpdateGithubEndpointOK, error) SetTransport(transport runtime.ClientTransport) } +/* +CreateGiteaEndpoint creates a gitea endpoint +*/ +func (a *Client) CreateGiteaEndpoint(params *CreateGiteaEndpointParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*CreateGiteaEndpointOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewCreateGiteaEndpointParams() + } + op := &runtime.ClientOperation{ + ID: "CreateGiteaEndpoint", + Method: "POST", + PathPattern: "/gitea/endpoints", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &CreateGiteaEndpointReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*CreateGiteaEndpointOK) + if ok { + return success, nil + } + // unexpected success response + unexpectedSuccess := result.(*CreateGiteaEndpointDefault) + return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code()) +} + /* CreateGithubEndpoint creates a git hub endpoint */ @@ -105,6 +153,38 @@ func (a *Client) CreateGithubEndpoint(params *CreateGithubEndpointParams, authIn return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code()) } +/* +DeleteGiteaEndpoint deletes a gitea endpoint +*/ +func (a *Client) DeleteGiteaEndpoint(params *DeleteGiteaEndpointParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) error { + // TODO: Validate the params before sending + if params == nil { + params = NewDeleteGiteaEndpointParams() + } + op := &runtime.ClientOperation{ + ID: "DeleteGiteaEndpoint", + Method: "DELETE", + PathPattern: "/gitea/endpoints/{name}", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &DeleteGiteaEndpointReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + _, err := a.transport.Submit(op) + if err != nil { + return err + } + return nil +} + /* DeleteGithubEndpoint deletes a git hub endpoint */ @@ -137,6 +217,44 @@ func (a *Client) DeleteGithubEndpoint(params *DeleteGithubEndpointParams, authIn return nil } +/* +GetGiteaEndpoint gets a gitea endpoint +*/ +func (a *Client) GetGiteaEndpoint(params *GetGiteaEndpointParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetGiteaEndpointOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewGetGiteaEndpointParams() + } + op := &runtime.ClientOperation{ + ID: "GetGiteaEndpoint", + Method: "GET", + PathPattern: "/gitea/endpoints/{name}", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &GetGiteaEndpointReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*GetGiteaEndpointOK) + if ok { + return success, nil + } + // unexpected success response + unexpectedSuccess := result.(*GetGiteaEndpointDefault) + return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code()) +} + /* GetGithubEndpoint gets a git hub endpoint */ @@ -175,6 +293,44 @@ func (a *Client) GetGithubEndpoint(params *GetGithubEndpointParams, authInfo run return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code()) } +/* +ListGiteaEndpoints lists all gitea endpoints +*/ +func (a *Client) ListGiteaEndpoints(params *ListGiteaEndpointsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*ListGiteaEndpointsOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewListGiteaEndpointsParams() + } + op := &runtime.ClientOperation{ + ID: "ListGiteaEndpoints", + Method: "GET", + PathPattern: "/gitea/endpoints", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &ListGiteaEndpointsReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*ListGiteaEndpointsOK) + if ok { + return success, nil + } + // unexpected success response + unexpectedSuccess := result.(*ListGiteaEndpointsDefault) + return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code()) +} + /* ListGithubEndpoints lists all git hub endpoints */ @@ -213,6 +369,44 @@ func (a *Client) ListGithubEndpoints(params *ListGithubEndpointsParams, authInfo return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code()) } +/* +UpdateGiteaEndpoint updates a gitea endpoint +*/ +func (a *Client) UpdateGiteaEndpoint(params *UpdateGiteaEndpointParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UpdateGiteaEndpointOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewUpdateGiteaEndpointParams() + } + op := &runtime.ClientOperation{ + ID: "UpdateGiteaEndpoint", + Method: "PUT", + PathPattern: "/gitea/endpoints/{name}", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &UpdateGiteaEndpointReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*UpdateGiteaEndpointOK) + if ok { + return success, nil + } + // unexpected success response + unexpectedSuccess := result.(*UpdateGiteaEndpointDefault) + return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code()) +} + /* UpdateGithubEndpoint updates a git hub endpoint */ diff --git a/client/endpoints/get_gitea_endpoint_parameters.go b/client/endpoints/get_gitea_endpoint_parameters.go new file mode 100644 index 000000000..0d7f883b4 --- /dev/null +++ b/client/endpoints/get_gitea_endpoint_parameters.go @@ -0,0 +1,151 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package endpoints + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" +) + +// NewGetGiteaEndpointParams creates a new GetGiteaEndpointParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewGetGiteaEndpointParams() *GetGiteaEndpointParams { + return &GetGiteaEndpointParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewGetGiteaEndpointParamsWithTimeout creates a new GetGiteaEndpointParams object +// with the ability to set a timeout on a request. +func NewGetGiteaEndpointParamsWithTimeout(timeout time.Duration) *GetGiteaEndpointParams { + return &GetGiteaEndpointParams{ + timeout: timeout, + } +} + +// NewGetGiteaEndpointParamsWithContext creates a new GetGiteaEndpointParams object +// with the ability to set a context for a request. +func NewGetGiteaEndpointParamsWithContext(ctx context.Context) *GetGiteaEndpointParams { + return &GetGiteaEndpointParams{ + Context: ctx, + } +} + +// NewGetGiteaEndpointParamsWithHTTPClient creates a new GetGiteaEndpointParams object +// with the ability to set a custom HTTPClient for a request. +func NewGetGiteaEndpointParamsWithHTTPClient(client *http.Client) *GetGiteaEndpointParams { + return &GetGiteaEndpointParams{ + HTTPClient: client, + } +} + +/* +GetGiteaEndpointParams contains all the parameters to send to the API endpoint + + for the get gitea endpoint operation. + + Typically these are written to a http.Request. +*/ +type GetGiteaEndpointParams struct { + + /* Name. + + The name of the Gitea endpoint. + */ + Name string + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the get gitea endpoint params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *GetGiteaEndpointParams) WithDefaults() *GetGiteaEndpointParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the get gitea endpoint params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *GetGiteaEndpointParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the get gitea endpoint params +func (o *GetGiteaEndpointParams) WithTimeout(timeout time.Duration) *GetGiteaEndpointParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the get gitea endpoint params +func (o *GetGiteaEndpointParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the get gitea endpoint params +func (o *GetGiteaEndpointParams) WithContext(ctx context.Context) *GetGiteaEndpointParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the get gitea endpoint params +func (o *GetGiteaEndpointParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the get gitea endpoint params +func (o *GetGiteaEndpointParams) WithHTTPClient(client *http.Client) *GetGiteaEndpointParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the get gitea endpoint params +func (o *GetGiteaEndpointParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithName adds the name to the get gitea endpoint params +func (o *GetGiteaEndpointParams) WithName(name string) *GetGiteaEndpointParams { + o.SetName(name) + return o +} + +// SetName adds the name to the get gitea endpoint params +func (o *GetGiteaEndpointParams) SetName(name string) { + o.Name = name +} + +// WriteToRequest writes these params to a swagger request +func (o *GetGiteaEndpointParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + // path param name + if err := r.SetPathParam("name", o.Name); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/client/endpoints/get_gitea_endpoint_responses.go b/client/endpoints/get_gitea_endpoint_responses.go new file mode 100644 index 000000000..e4bacd033 --- /dev/null +++ b/client/endpoints/get_gitea_endpoint_responses.go @@ -0,0 +1,184 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package endpoints + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + apiserver_params "github.com/cloudbase/garm/apiserver/params" + garm_params "github.com/cloudbase/garm/params" +) + +// GetGiteaEndpointReader is a Reader for the GetGiteaEndpoint structure. +type GetGiteaEndpointReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *GetGiteaEndpointReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewGetGiteaEndpointOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + default: + result := NewGetGiteaEndpointDefault(response.Code()) + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + if response.Code()/100 == 2 { + return result, nil + } + return nil, result + } +} + +// NewGetGiteaEndpointOK creates a GetGiteaEndpointOK with default headers values +func NewGetGiteaEndpointOK() *GetGiteaEndpointOK { + return &GetGiteaEndpointOK{} +} + +/* +GetGiteaEndpointOK describes a response with status code 200, with default header values. + +ForgeEndpoint +*/ +type GetGiteaEndpointOK struct { + Payload garm_params.ForgeEndpoint +} + +// IsSuccess returns true when this get gitea endpoint o k response has a 2xx status code +func (o *GetGiteaEndpointOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this get gitea endpoint o k response has a 3xx status code +func (o *GetGiteaEndpointOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this get gitea endpoint o k response has a 4xx status code +func (o *GetGiteaEndpointOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this get gitea endpoint o k response has a 5xx status code +func (o *GetGiteaEndpointOK) IsServerError() bool { + return false +} + +// IsCode returns true when this get gitea endpoint o k response a status code equal to that given +func (o *GetGiteaEndpointOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the get gitea endpoint o k response +func (o *GetGiteaEndpointOK) Code() int { + return 200 +} + +func (o *GetGiteaEndpointOK) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /gitea/endpoints/{name}][%d] getGiteaEndpointOK %s", 200, payload) +} + +func (o *GetGiteaEndpointOK) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /gitea/endpoints/{name}][%d] getGiteaEndpointOK %s", 200, payload) +} + +func (o *GetGiteaEndpointOK) GetPayload() garm_params.ForgeEndpoint { + return o.Payload +} + +func (o *GetGiteaEndpointOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewGetGiteaEndpointDefault creates a GetGiteaEndpointDefault with default headers values +func NewGetGiteaEndpointDefault(code int) *GetGiteaEndpointDefault { + return &GetGiteaEndpointDefault{ + _statusCode: code, + } +} + +/* +GetGiteaEndpointDefault describes a response with status code -1, with default header values. + +APIErrorResponse +*/ +type GetGiteaEndpointDefault struct { + _statusCode int + + Payload apiserver_params.APIErrorResponse +} + +// IsSuccess returns true when this get gitea endpoint default response has a 2xx status code +func (o *GetGiteaEndpointDefault) IsSuccess() bool { + return o._statusCode/100 == 2 +} + +// IsRedirect returns true when this get gitea endpoint default response has a 3xx status code +func (o *GetGiteaEndpointDefault) IsRedirect() bool { + return o._statusCode/100 == 3 +} + +// IsClientError returns true when this get gitea endpoint default response has a 4xx status code +func (o *GetGiteaEndpointDefault) IsClientError() bool { + return o._statusCode/100 == 4 +} + +// IsServerError returns true when this get gitea endpoint default response has a 5xx status code +func (o *GetGiteaEndpointDefault) IsServerError() bool { + return o._statusCode/100 == 5 +} + +// IsCode returns true when this get gitea endpoint default response a status code equal to that given +func (o *GetGiteaEndpointDefault) IsCode(code int) bool { + return o._statusCode == code +} + +// Code gets the status code for the get gitea endpoint default response +func (o *GetGiteaEndpointDefault) Code() int { + return o._statusCode +} + +func (o *GetGiteaEndpointDefault) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /gitea/endpoints/{name}][%d] GetGiteaEndpoint default %s", o._statusCode, payload) +} + +func (o *GetGiteaEndpointDefault) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /gitea/endpoints/{name}][%d] GetGiteaEndpoint default %s", o._statusCode, payload) +} + +func (o *GetGiteaEndpointDefault) GetPayload() apiserver_params.APIErrorResponse { + return o.Payload +} + +func (o *GetGiteaEndpointDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/client/endpoints/get_github_endpoint_responses.go b/client/endpoints/get_github_endpoint_responses.go index d84f9280b..e2b97a600 100644 --- a/client/endpoints/get_github_endpoint_responses.go +++ b/client/endpoints/get_github_endpoint_responses.go @@ -51,10 +51,10 @@ func NewGetGithubEndpointOK() *GetGithubEndpointOK { /* GetGithubEndpointOK describes a response with status code 200, with default header values. -GithubEndpoint +ForgeEndpoint */ type GetGithubEndpointOK struct { - Payload garm_params.GithubEndpoint + Payload garm_params.ForgeEndpoint } // IsSuccess returns true when this get github endpoint o k response has a 2xx status code @@ -97,7 +97,7 @@ func (o *GetGithubEndpointOK) String() string { return fmt.Sprintf("[GET /github/endpoints/{name}][%d] getGithubEndpointOK %s", 200, payload) } -func (o *GetGithubEndpointOK) GetPayload() garm_params.GithubEndpoint { +func (o *GetGithubEndpointOK) GetPayload() garm_params.ForgeEndpoint { return o.Payload } diff --git a/client/endpoints/list_gitea_endpoints_parameters.go b/client/endpoints/list_gitea_endpoints_parameters.go new file mode 100644 index 000000000..93ec6ae61 --- /dev/null +++ b/client/endpoints/list_gitea_endpoints_parameters.go @@ -0,0 +1,128 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package endpoints + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" +) + +// NewListGiteaEndpointsParams creates a new ListGiteaEndpointsParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewListGiteaEndpointsParams() *ListGiteaEndpointsParams { + return &ListGiteaEndpointsParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewListGiteaEndpointsParamsWithTimeout creates a new ListGiteaEndpointsParams object +// with the ability to set a timeout on a request. +func NewListGiteaEndpointsParamsWithTimeout(timeout time.Duration) *ListGiteaEndpointsParams { + return &ListGiteaEndpointsParams{ + timeout: timeout, + } +} + +// NewListGiteaEndpointsParamsWithContext creates a new ListGiteaEndpointsParams object +// with the ability to set a context for a request. +func NewListGiteaEndpointsParamsWithContext(ctx context.Context) *ListGiteaEndpointsParams { + return &ListGiteaEndpointsParams{ + Context: ctx, + } +} + +// NewListGiteaEndpointsParamsWithHTTPClient creates a new ListGiteaEndpointsParams object +// with the ability to set a custom HTTPClient for a request. +func NewListGiteaEndpointsParamsWithHTTPClient(client *http.Client) *ListGiteaEndpointsParams { + return &ListGiteaEndpointsParams{ + HTTPClient: client, + } +} + +/* +ListGiteaEndpointsParams contains all the parameters to send to the API endpoint + + for the list gitea endpoints operation. + + Typically these are written to a http.Request. +*/ +type ListGiteaEndpointsParams struct { + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the list gitea endpoints params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *ListGiteaEndpointsParams) WithDefaults() *ListGiteaEndpointsParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the list gitea endpoints params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *ListGiteaEndpointsParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the list gitea endpoints params +func (o *ListGiteaEndpointsParams) WithTimeout(timeout time.Duration) *ListGiteaEndpointsParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the list gitea endpoints params +func (o *ListGiteaEndpointsParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the list gitea endpoints params +func (o *ListGiteaEndpointsParams) WithContext(ctx context.Context) *ListGiteaEndpointsParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the list gitea endpoints params +func (o *ListGiteaEndpointsParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the list gitea endpoints params +func (o *ListGiteaEndpointsParams) WithHTTPClient(client *http.Client) *ListGiteaEndpointsParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the list gitea endpoints params +func (o *ListGiteaEndpointsParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WriteToRequest writes these params to a swagger request +func (o *ListGiteaEndpointsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/client/endpoints/list_gitea_endpoints_responses.go b/client/endpoints/list_gitea_endpoints_responses.go new file mode 100644 index 000000000..0fdd90ec5 --- /dev/null +++ b/client/endpoints/list_gitea_endpoints_responses.go @@ -0,0 +1,184 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package endpoints + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + apiserver_params "github.com/cloudbase/garm/apiserver/params" + garm_params "github.com/cloudbase/garm/params" +) + +// ListGiteaEndpointsReader is a Reader for the ListGiteaEndpoints structure. +type ListGiteaEndpointsReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *ListGiteaEndpointsReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewListGiteaEndpointsOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + default: + result := NewListGiteaEndpointsDefault(response.Code()) + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + if response.Code()/100 == 2 { + return result, nil + } + return nil, result + } +} + +// NewListGiteaEndpointsOK creates a ListGiteaEndpointsOK with default headers values +func NewListGiteaEndpointsOK() *ListGiteaEndpointsOK { + return &ListGiteaEndpointsOK{} +} + +/* +ListGiteaEndpointsOK describes a response with status code 200, with default header values. + +ForgeEndpoints +*/ +type ListGiteaEndpointsOK struct { + Payload garm_params.ForgeEndpoints +} + +// IsSuccess returns true when this list gitea endpoints o k response has a 2xx status code +func (o *ListGiteaEndpointsOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this list gitea endpoints o k response has a 3xx status code +func (o *ListGiteaEndpointsOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this list gitea endpoints o k response has a 4xx status code +func (o *ListGiteaEndpointsOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this list gitea endpoints o k response has a 5xx status code +func (o *ListGiteaEndpointsOK) IsServerError() bool { + return false +} + +// IsCode returns true when this list gitea endpoints o k response a status code equal to that given +func (o *ListGiteaEndpointsOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the list gitea endpoints o k response +func (o *ListGiteaEndpointsOK) Code() int { + return 200 +} + +func (o *ListGiteaEndpointsOK) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /gitea/endpoints][%d] listGiteaEndpointsOK %s", 200, payload) +} + +func (o *ListGiteaEndpointsOK) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /gitea/endpoints][%d] listGiteaEndpointsOK %s", 200, payload) +} + +func (o *ListGiteaEndpointsOK) GetPayload() garm_params.ForgeEndpoints { + return o.Payload +} + +func (o *ListGiteaEndpointsOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewListGiteaEndpointsDefault creates a ListGiteaEndpointsDefault with default headers values +func NewListGiteaEndpointsDefault(code int) *ListGiteaEndpointsDefault { + return &ListGiteaEndpointsDefault{ + _statusCode: code, + } +} + +/* +ListGiteaEndpointsDefault describes a response with status code -1, with default header values. + +APIErrorResponse +*/ +type ListGiteaEndpointsDefault struct { + _statusCode int + + Payload apiserver_params.APIErrorResponse +} + +// IsSuccess returns true when this list gitea endpoints default response has a 2xx status code +func (o *ListGiteaEndpointsDefault) IsSuccess() bool { + return o._statusCode/100 == 2 +} + +// IsRedirect returns true when this list gitea endpoints default response has a 3xx status code +func (o *ListGiteaEndpointsDefault) IsRedirect() bool { + return o._statusCode/100 == 3 +} + +// IsClientError returns true when this list gitea endpoints default response has a 4xx status code +func (o *ListGiteaEndpointsDefault) IsClientError() bool { + return o._statusCode/100 == 4 +} + +// IsServerError returns true when this list gitea endpoints default response has a 5xx status code +func (o *ListGiteaEndpointsDefault) IsServerError() bool { + return o._statusCode/100 == 5 +} + +// IsCode returns true when this list gitea endpoints default response a status code equal to that given +func (o *ListGiteaEndpointsDefault) IsCode(code int) bool { + return o._statusCode == code +} + +// Code gets the status code for the list gitea endpoints default response +func (o *ListGiteaEndpointsDefault) Code() int { + return o._statusCode +} + +func (o *ListGiteaEndpointsDefault) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /gitea/endpoints][%d] ListGiteaEndpoints default %s", o._statusCode, payload) +} + +func (o *ListGiteaEndpointsDefault) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /gitea/endpoints][%d] ListGiteaEndpoints default %s", o._statusCode, payload) +} + +func (o *ListGiteaEndpointsDefault) GetPayload() apiserver_params.APIErrorResponse { + return o.Payload +} + +func (o *ListGiteaEndpointsDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/client/endpoints/list_github_endpoints_responses.go b/client/endpoints/list_github_endpoints_responses.go index 6c2dde6ca..33485f9b0 100644 --- a/client/endpoints/list_github_endpoints_responses.go +++ b/client/endpoints/list_github_endpoints_responses.go @@ -51,10 +51,10 @@ func NewListGithubEndpointsOK() *ListGithubEndpointsOK { /* ListGithubEndpointsOK describes a response with status code 200, with default header values. -GithubEndpoints +ForgeEndpoints */ type ListGithubEndpointsOK struct { - Payload garm_params.GithubEndpoints + Payload garm_params.ForgeEndpoints } // IsSuccess returns true when this list github endpoints o k response has a 2xx status code @@ -97,7 +97,7 @@ func (o *ListGithubEndpointsOK) String() string { return fmt.Sprintf("[GET /github/endpoints][%d] listGithubEndpointsOK %s", 200, payload) } -func (o *ListGithubEndpointsOK) GetPayload() garm_params.GithubEndpoints { +func (o *ListGithubEndpointsOK) GetPayload() garm_params.ForgeEndpoints { return o.Payload } diff --git a/client/endpoints/update_gitea_endpoint_parameters.go b/client/endpoints/update_gitea_endpoint_parameters.go new file mode 100644 index 000000000..bfd18e2eb --- /dev/null +++ b/client/endpoints/update_gitea_endpoint_parameters.go @@ -0,0 +1,173 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package endpoints + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + + garm_params "github.com/cloudbase/garm/params" +) + +// NewUpdateGiteaEndpointParams creates a new UpdateGiteaEndpointParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewUpdateGiteaEndpointParams() *UpdateGiteaEndpointParams { + return &UpdateGiteaEndpointParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewUpdateGiteaEndpointParamsWithTimeout creates a new UpdateGiteaEndpointParams object +// with the ability to set a timeout on a request. +func NewUpdateGiteaEndpointParamsWithTimeout(timeout time.Duration) *UpdateGiteaEndpointParams { + return &UpdateGiteaEndpointParams{ + timeout: timeout, + } +} + +// NewUpdateGiteaEndpointParamsWithContext creates a new UpdateGiteaEndpointParams object +// with the ability to set a context for a request. +func NewUpdateGiteaEndpointParamsWithContext(ctx context.Context) *UpdateGiteaEndpointParams { + return &UpdateGiteaEndpointParams{ + Context: ctx, + } +} + +// NewUpdateGiteaEndpointParamsWithHTTPClient creates a new UpdateGiteaEndpointParams object +// with the ability to set a custom HTTPClient for a request. +func NewUpdateGiteaEndpointParamsWithHTTPClient(client *http.Client) *UpdateGiteaEndpointParams { + return &UpdateGiteaEndpointParams{ + HTTPClient: client, + } +} + +/* +UpdateGiteaEndpointParams contains all the parameters to send to the API endpoint + + for the update gitea endpoint operation. + + Typically these are written to a http.Request. +*/ +type UpdateGiteaEndpointParams struct { + + /* Body. + + Parameters used when updating a Gitea endpoint. + */ + Body garm_params.UpdateGiteaEndpointParams + + /* Name. + + The name of the Gitea endpoint. + */ + Name string + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the update gitea endpoint params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *UpdateGiteaEndpointParams) WithDefaults() *UpdateGiteaEndpointParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the update gitea endpoint params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *UpdateGiteaEndpointParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the update gitea endpoint params +func (o *UpdateGiteaEndpointParams) WithTimeout(timeout time.Duration) *UpdateGiteaEndpointParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the update gitea endpoint params +func (o *UpdateGiteaEndpointParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the update gitea endpoint params +func (o *UpdateGiteaEndpointParams) WithContext(ctx context.Context) *UpdateGiteaEndpointParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the update gitea endpoint params +func (o *UpdateGiteaEndpointParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the update gitea endpoint params +func (o *UpdateGiteaEndpointParams) WithHTTPClient(client *http.Client) *UpdateGiteaEndpointParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the update gitea endpoint params +func (o *UpdateGiteaEndpointParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithBody adds the body to the update gitea endpoint params +func (o *UpdateGiteaEndpointParams) WithBody(body garm_params.UpdateGiteaEndpointParams) *UpdateGiteaEndpointParams { + o.SetBody(body) + return o +} + +// SetBody adds the body to the update gitea endpoint params +func (o *UpdateGiteaEndpointParams) SetBody(body garm_params.UpdateGiteaEndpointParams) { + o.Body = body +} + +// WithName adds the name to the update gitea endpoint params +func (o *UpdateGiteaEndpointParams) WithName(name string) *UpdateGiteaEndpointParams { + o.SetName(name) + return o +} + +// SetName adds the name to the update gitea endpoint params +func (o *UpdateGiteaEndpointParams) SetName(name string) { + o.Name = name +} + +// WriteToRequest writes these params to a swagger request +func (o *UpdateGiteaEndpointParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + if err := r.SetBodyParam(o.Body); err != nil { + return err + } + + // path param name + if err := r.SetPathParam("name", o.Name); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/client/endpoints/update_gitea_endpoint_responses.go b/client/endpoints/update_gitea_endpoint_responses.go new file mode 100644 index 000000000..052f45fa2 --- /dev/null +++ b/client/endpoints/update_gitea_endpoint_responses.go @@ -0,0 +1,184 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package endpoints + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + apiserver_params "github.com/cloudbase/garm/apiserver/params" + garm_params "github.com/cloudbase/garm/params" +) + +// UpdateGiteaEndpointReader is a Reader for the UpdateGiteaEndpoint structure. +type UpdateGiteaEndpointReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *UpdateGiteaEndpointReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewUpdateGiteaEndpointOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + default: + result := NewUpdateGiteaEndpointDefault(response.Code()) + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + if response.Code()/100 == 2 { + return result, nil + } + return nil, result + } +} + +// NewUpdateGiteaEndpointOK creates a UpdateGiteaEndpointOK with default headers values +func NewUpdateGiteaEndpointOK() *UpdateGiteaEndpointOK { + return &UpdateGiteaEndpointOK{} +} + +/* +UpdateGiteaEndpointOK describes a response with status code 200, with default header values. + +ForgeEndpoint +*/ +type UpdateGiteaEndpointOK struct { + Payload garm_params.ForgeEndpoint +} + +// IsSuccess returns true when this update gitea endpoint o k response has a 2xx status code +func (o *UpdateGiteaEndpointOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this update gitea endpoint o k response has a 3xx status code +func (o *UpdateGiteaEndpointOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this update gitea endpoint o k response has a 4xx status code +func (o *UpdateGiteaEndpointOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this update gitea endpoint o k response has a 5xx status code +func (o *UpdateGiteaEndpointOK) IsServerError() bool { + return false +} + +// IsCode returns true when this update gitea endpoint o k response a status code equal to that given +func (o *UpdateGiteaEndpointOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the update gitea endpoint o k response +func (o *UpdateGiteaEndpointOK) Code() int { + return 200 +} + +func (o *UpdateGiteaEndpointOK) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PUT /gitea/endpoints/{name}][%d] updateGiteaEndpointOK %s", 200, payload) +} + +func (o *UpdateGiteaEndpointOK) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PUT /gitea/endpoints/{name}][%d] updateGiteaEndpointOK %s", 200, payload) +} + +func (o *UpdateGiteaEndpointOK) GetPayload() garm_params.ForgeEndpoint { + return o.Payload +} + +func (o *UpdateGiteaEndpointOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewUpdateGiteaEndpointDefault creates a UpdateGiteaEndpointDefault with default headers values +func NewUpdateGiteaEndpointDefault(code int) *UpdateGiteaEndpointDefault { + return &UpdateGiteaEndpointDefault{ + _statusCode: code, + } +} + +/* +UpdateGiteaEndpointDefault describes a response with status code -1, with default header values. + +APIErrorResponse +*/ +type UpdateGiteaEndpointDefault struct { + _statusCode int + + Payload apiserver_params.APIErrorResponse +} + +// IsSuccess returns true when this update gitea endpoint default response has a 2xx status code +func (o *UpdateGiteaEndpointDefault) IsSuccess() bool { + return o._statusCode/100 == 2 +} + +// IsRedirect returns true when this update gitea endpoint default response has a 3xx status code +func (o *UpdateGiteaEndpointDefault) IsRedirect() bool { + return o._statusCode/100 == 3 +} + +// IsClientError returns true when this update gitea endpoint default response has a 4xx status code +func (o *UpdateGiteaEndpointDefault) IsClientError() bool { + return o._statusCode/100 == 4 +} + +// IsServerError returns true when this update gitea endpoint default response has a 5xx status code +func (o *UpdateGiteaEndpointDefault) IsServerError() bool { + return o._statusCode/100 == 5 +} + +// IsCode returns true when this update gitea endpoint default response a status code equal to that given +func (o *UpdateGiteaEndpointDefault) IsCode(code int) bool { + return o._statusCode == code +} + +// Code gets the status code for the update gitea endpoint default response +func (o *UpdateGiteaEndpointDefault) Code() int { + return o._statusCode +} + +func (o *UpdateGiteaEndpointDefault) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PUT /gitea/endpoints/{name}][%d] UpdateGiteaEndpoint default %s", o._statusCode, payload) +} + +func (o *UpdateGiteaEndpointDefault) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PUT /gitea/endpoints/{name}][%d] UpdateGiteaEndpoint default %s", o._statusCode, payload) +} + +func (o *UpdateGiteaEndpointDefault) GetPayload() apiserver_params.APIErrorResponse { + return o.Payload +} + +func (o *UpdateGiteaEndpointDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/client/endpoints/update_github_endpoint_responses.go b/client/endpoints/update_github_endpoint_responses.go index 234ed7116..27cd4a71d 100644 --- a/client/endpoints/update_github_endpoint_responses.go +++ b/client/endpoints/update_github_endpoint_responses.go @@ -51,10 +51,10 @@ func NewUpdateGithubEndpointOK() *UpdateGithubEndpointOK { /* UpdateGithubEndpointOK describes a response with status code 200, with default header values. -GithubEndpoint +ForgeEndpoint */ type UpdateGithubEndpointOK struct { - Payload garm_params.GithubEndpoint + Payload garm_params.ForgeEndpoint } // IsSuccess returns true when this update github endpoint o k response has a 2xx status code @@ -97,7 +97,7 @@ func (o *UpdateGithubEndpointOK) String() string { return fmt.Sprintf("[PUT /github/endpoints/{name}][%d] updateGithubEndpointOK %s", 200, payload) } -func (o *UpdateGithubEndpointOK) GetPayload() garm_params.GithubEndpoint { +func (o *UpdateGithubEndpointOK) GetPayload() garm_params.ForgeEndpoint { return o.Payload } diff --git a/cmd/garm-cli/cmd/events.go b/cmd/garm-cli/cmd/events.go index f38e9ea66..da44732af 100644 --- a/cmd/garm-cli/cmd/events.go +++ b/cmd/garm-cli/cmd/events.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package cmd import ( diff --git a/cmd/garm-cli/cmd/gitea.go b/cmd/garm-cli/cmd/gitea.go new file mode 100644 index 000000000..6627fd6fd --- /dev/null +++ b/cmd/garm-cli/cmd/gitea.go @@ -0,0 +1,34 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +package cmd + +import "github.com/spf13/cobra" + +// giteaCmd represents the the gitea command. This command has a set +// of subcommands that allow configuring and managing Gitea endpoints +// and credentials. +var giteaCmd = &cobra.Command{ + Use: "gitea", + Aliases: []string{"gt"}, + SilenceUsage: true, + Short: "Manage Gitea resources", + Long: `Manage Gitea related resources. + +This command allows you to configure and manage Gitea endpoints and credentials`, + Run: nil, +} + +func init() { + rootCmd.AddCommand(giteaCmd) +} diff --git a/cmd/garm-cli/cmd/gitea_credentials.go b/cmd/garm-cli/cmd/gitea_credentials.go new file mode 100644 index 000000000..d26f95ed8 --- /dev/null +++ b/cmd/garm-cli/cmd/gitea_credentials.go @@ -0,0 +1,317 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cmd + +import ( + "fmt" + "strconv" + + "github.com/jedib0t/go-pretty/v6/table" + "github.com/spf13/cobra" + + apiClientCreds "github.com/cloudbase/garm/client/credentials" + "github.com/cloudbase/garm/cmd/garm-cli/common" + "github.com/cloudbase/garm/params" +) + +// giteaCredentialsCmd represents the gitea credentials command +var giteaCredentialsCmd = &cobra.Command{ + Use: "credentials", + Aliases: []string{"creds"}, + Short: "Manage gitea credentials", + Long: `Manage Gitea credentials stored in GARM. + +This command allows you to add, update, list and delete Gitea credentials.`, + Run: nil, +} + +var giteaCredentialsListCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List configured gitea credentials", + Long: `List the names of the gitea personal access tokens available to the garm.`, + SilenceUsage: true, + RunE: func(_ *cobra.Command, _ []string) error { + if needsInit { + return errNeedsInitError + } + + listCredsReq := apiClientCreds.NewListGiteaCredentialsParams() + response, err := apiCli.Credentials.ListGiteaCredentials(listCredsReq, authToken) + if err != nil { + return err + } + formatGiteaCredentials(response.Payload) + return nil + }, +} + +var giteaCredentialsShowCmd = &cobra.Command{ + Use: "show", + Aliases: []string{"get"}, + Short: "Show details of a configured gitea credential", + Long: `Show the details of a configured gitea credential.`, + SilenceUsage: true, + RunE: func(_ *cobra.Command, args []string) error { + if needsInit { + return errNeedsInitError + } + + if len(args) < 1 { + return fmt.Errorf("missing required argument: credential ID") + } + + credID, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return fmt.Errorf("invalid credential ID: %s", args[0]) + } + showCredsReq := apiClientCreds.NewGetGiteaCredentialsParams().WithID(credID) + response, err := apiCli.Credentials.GetGiteaCredentials(showCredsReq, authToken) + if err != nil { + return err + } + formatOneGiteaCredential(response.Payload) + return nil + }, +} + +var giteaCredentialsUpdateCmd = &cobra.Command{ + Use: "update", + Short: "Update a gitea credential", + Long: "Update a gitea credential", + SilenceUsage: true, + RunE: func(_ *cobra.Command, args []string) error { + if needsInit { + return errNeedsInitError + } + + if len(args) < 1 { + return fmt.Errorf("missing required argument: credential ID") + } + + if len(args) > 1 { + return fmt.Errorf("too many arguments") + } + + credID, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return fmt.Errorf("invalid credential ID: %s", args[0]) + } + + updateParams, err := parseGiteaCredentialsUpdateParams() + if err != nil { + return err + } + + updateCredsReq := apiClientCreds.NewUpdateGiteaCredentialsParams().WithID(credID) + updateCredsReq.Body = updateParams + + response, err := apiCli.Credentials.UpdateGiteaCredentials(updateCredsReq, authToken) + if err != nil { + return err + } + formatOneGiteaCredential(response.Payload) + return nil + }, +} + +var giteaCredentialsDeleteCmd = &cobra.Command{ + Use: "delete", + Aliases: []string{"remove", "rm"}, + Short: "Delete a gitea credential", + Long: "Delete a gitea credential", + SilenceUsage: true, + RunE: func(_ *cobra.Command, args []string) error { + if needsInit { + return errNeedsInitError + } + + if len(args) < 1 { + return fmt.Errorf("missing required argument: credential ID") + } + + if len(args) > 1 { + return fmt.Errorf("too many arguments") + } + + credID, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return fmt.Errorf("invalid credential ID: %s", args[0]) + } + + deleteCredsReq := apiClientCreds.NewDeleteGiteaCredentialsParams().WithID(credID) + if err := apiCli.Credentials.DeleteGiteaCredentials(deleteCredsReq, authToken); err != nil { + return err + } + return nil + }, +} + +var giteaCredentialsAddCmd = &cobra.Command{ + Use: "add", + Short: "Add a gitea credential", + Long: "Add a gitea credential", + SilenceUsage: true, + RunE: func(_ *cobra.Command, args []string) error { + if needsInit { + return errNeedsInitError + } + + if len(args) > 0 { + return fmt.Errorf("too many arguments") + } + + addParams, err := parseGiteaCredentialsAddParams() + if err != nil { + return err + } + + addCredsReq := apiClientCreds.NewCreateGiteaCredentialsParams() + addCredsReq.Body = addParams + + response, err := apiCli.Credentials.CreateGiteaCredentials(addCredsReq, authToken) + if err != nil { + return err + } + formatOneGiteaCredential(response.Payload) + return nil + }, +} + +func init() { + giteaCredentialsUpdateCmd.Flags().StringVar(&credentialsName, "name", "", "Name of the credential") + giteaCredentialsUpdateCmd.Flags().StringVar(&credentialsDescription, "description", "", "Description of the credential") + giteaCredentialsUpdateCmd.Flags().StringVar(&credentialsOAuthToken, "pat-oauth-token", "", "If the credential is a personal access token, the OAuth token") + + giteaCredentialsListCmd.Flags().BoolVarP(&long, "long", "l", false, "Include additional info.") + + giteaCredentialsAddCmd.Flags().StringVar(&credentialsName, "name", "", "Name of the credential") + giteaCredentialsAddCmd.Flags().StringVar(&credentialsDescription, "description", "", "Description of the credential") + giteaCredentialsAddCmd.Flags().StringVar(&credentialsOAuthToken, "pat-oauth-token", "", "If the credential is a personal access token, the OAuth token") + giteaCredentialsAddCmd.Flags().StringVar(&credentialsType, "auth-type", "", "The type of the credential") + giteaCredentialsAddCmd.Flags().StringVar(&credentialsEndpoint, "endpoint", "", "The endpoint to associate the credential with") + + giteaCredentialsAddCmd.MarkFlagRequired("name") + giteaCredentialsAddCmd.MarkFlagRequired("auth-type") + giteaCredentialsAddCmd.MarkFlagRequired("description") + giteaCredentialsAddCmd.MarkFlagRequired("endpoint") + + giteaCredentialsCmd.AddCommand( + giteaCredentialsListCmd, + giteaCredentialsShowCmd, + giteaCredentialsUpdateCmd, + giteaCredentialsDeleteCmd, + giteaCredentialsAddCmd, + ) + giteaCmd.AddCommand(giteaCredentialsCmd) +} + +func parseGiteaCredentialsAddParams() (ret params.CreateGiteaCredentialsParams, err error) { + ret.Name = credentialsName + ret.Description = credentialsDescription + ret.AuthType = params.ForgeAuthType(credentialsType) + ret.Endpoint = credentialsEndpoint + switch ret.AuthType { + case params.ForgeAuthTypePAT: + ret.PAT.OAuth2Token = credentialsOAuthToken + default: + return params.CreateGiteaCredentialsParams{}, fmt.Errorf("invalid auth type: %s (supported are: pat)", credentialsType) + } + + return ret, nil +} + +func parseGiteaCredentialsUpdateParams() (params.UpdateGiteaCredentialsParams, error) { + var updateParams params.UpdateGiteaCredentialsParams + + if credentialsName != "" { + updateParams.Name = &credentialsName + } + + if credentialsDescription != "" { + updateParams.Description = &credentialsDescription + } + + if credentialsOAuthToken != "" { + if updateParams.PAT == nil { + updateParams.PAT = ¶ms.GithubPAT{} + } + updateParams.PAT.OAuth2Token = credentialsOAuthToken + } + + return updateParams, nil +} + +func formatGiteaCredentials(creds []params.ForgeCredentials) { + if outputFormat == common.OutputFormatJSON { + printAsJSON(creds) + return + } + t := table.NewWriter() + header := table.Row{"ID", "Name", "Description", "Base URL", "API URL", "Type"} + if long { + header = append(header, "Created At", "Updated At") + } + t.AppendHeader(header) + for _, val := range creds { + row := table.Row{val.ID, val.Name, val.Description, val.BaseURL, val.APIBaseURL, val.AuthType} + if long { + row = append(row, val.CreatedAt, val.UpdatedAt) + } + t.AppendRow(row) + t.AppendSeparator() + } + fmt.Println(t.Render()) +} + +func formatOneGiteaCredential(cred params.ForgeCredentials) { + if outputFormat == common.OutputFormatJSON { + printAsJSON(cred) + return + } + t := table.NewWriter() + header := table.Row{"Field", "Value"} + t.AppendHeader(header) + + t.AppendRow(table.Row{"ID", cred.ID}) + t.AppendRow(table.Row{"Created At", cred.CreatedAt}) + t.AppendRow(table.Row{"Updated At", cred.UpdatedAt}) + t.AppendRow(table.Row{"Name", cred.Name}) + t.AppendRow(table.Row{"Description", cred.Description}) + t.AppendRow(table.Row{"Base URL", cred.BaseURL}) + t.AppendRow(table.Row{"API URL", cred.APIBaseURL}) + t.AppendRow(table.Row{"Type", cred.AuthType}) + t.AppendRow(table.Row{"Endpoint", cred.Endpoint.Name}) + + if len(cred.Repositories) > 0 { + t.AppendRow(table.Row{"", ""}) + for _, repo := range cred.Repositories { + t.AppendRow(table.Row{"Repositories", repo.String()}) + } + } + + if len(cred.Organizations) > 0 { + t.AppendRow(table.Row{"", ""}) + for _, org := range cred.Organizations { + t.AppendRow(table.Row{"Organizations", org.Name}) + } + } + + t.SetColumnConfigs([]table.ColumnConfig{ + {Number: 1, AutoMerge: true}, + {Number: 2, AutoMerge: false, WidthMax: 100}, + }) + fmt.Println(t.Render()) +} diff --git a/cmd/garm-cli/cmd/gitea_endpoints.go b/cmd/garm-cli/cmd/gitea_endpoints.go new file mode 100644 index 000000000..55fa09c96 --- /dev/null +++ b/cmd/garm-cli/cmd/gitea_endpoints.go @@ -0,0 +1,231 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + apiClientEndpoints "github.com/cloudbase/garm/client/endpoints" + "github.com/cloudbase/garm/params" +) + +var giteaEndpointCmd = &cobra.Command{ + Use: "endpoint", + SilenceUsage: true, + Short: "Manage Gitea endpoints", + Long: `Manage Gitea endpoints. + +This command allows you to configure and manage Gitea endpoints`, + Run: nil, +} + +var giteaEndpointListCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + SilenceUsage: true, + Short: "List Gitea endpoints", + Long: `List all configured Gitea endpoints.`, + RunE: func(_ *cobra.Command, _ []string) error { + if needsInit { + return errNeedsInitError + } + + newListReq := apiClientEndpoints.NewListGiteaEndpointsParams() + response, err := apiCli.Endpoints.ListGiteaEndpoints(newListReq, authToken) + if err != nil { + return err + } + formatEndpoints(response.Payload) + return nil + }, +} + +var giteaEndpointShowCmd = &cobra.Command{ + Use: "show", + Aliases: []string{"get"}, + SilenceUsage: true, + Short: "Show Gitea endpoint", + Long: `Show details of a Gitea endpoint.`, + RunE: func(_ *cobra.Command, args []string) error { + if needsInit { + return errNeedsInitError + } + if len(args) == 0 { + return fmt.Errorf("requires an endpoint name") + } + if len(args) > 1 { + return fmt.Errorf("too many arguments") + } + + newShowReq := apiClientEndpoints.NewGetGiteaEndpointParams() + newShowReq.Name = args[0] + response, err := apiCli.Endpoints.GetGiteaEndpoint(newShowReq, authToken) + if err != nil { + return err + } + formatOneEndpoint(response.Payload) + return nil + }, +} + +var giteaEndpointCreateCmd = &cobra.Command{ + Use: "create", + SilenceUsage: true, + Short: "Create Gitea endpoint", + Long: `Create a new Gitea endpoint.`, + RunE: func(_ *cobra.Command, _ []string) error { + if needsInit { + return errNeedsInitError + } + + createParams, err := parseGiteaCreateParams() + if err != nil { + return err + } + + newCreateReq := apiClientEndpoints.NewCreateGiteaEndpointParams() + newCreateReq.Body = createParams + + response, err := apiCli.Endpoints.CreateGiteaEndpoint(newCreateReq, authToken) + if err != nil { + return err + } + formatOneEndpoint(response.Payload) + return nil + }, +} + +var giteaEndpointDeleteCmd = &cobra.Command{ + Use: "delete", + Aliases: []string{"remove", "rm"}, + SilenceUsage: true, + Short: "Delete Gitea endpoint", + Long: "Delete a Gitea endpoint", + RunE: func(_ *cobra.Command, args []string) error { + if needsInit { + return errNeedsInitError + } + if len(args) == 0 { + return fmt.Errorf("requires an endpoint name") + } + if len(args) > 1 { + return fmt.Errorf("too many arguments") + } + + newDeleteReq := apiClientEndpoints.NewDeleteGiteaEndpointParams() + newDeleteReq.Name = args[0] + if err := apiCli.Endpoints.DeleteGiteaEndpoint(newDeleteReq, authToken); err != nil { + return err + } + return nil + }, +} + +var giteaEndpointUpdateCmd = &cobra.Command{ + Use: "update", + Short: "Update Gitea endpoint", + Long: "Update a Gitea endpoint", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if needsInit { + return errNeedsInitError + } + if len(args) == 0 { + return fmt.Errorf("requires an endpoint name") + } + if len(args) > 1 { + return fmt.Errorf("too many arguments") + } + + updateParams := params.UpdateGiteaEndpointParams{} + + if cmd.Flags().Changed("ca-cert-path") { + cert, err := parseAndReadCABundle() + if err != nil { + return err + } + updateParams.CACertBundle = cert + } + + if cmd.Flags().Changed("description") { + updateParams.Description = &endpointDescription + } + + if cmd.Flags().Changed("base-url") { + updateParams.BaseURL = &endpointBaseURL + } + + if cmd.Flags().Changed("api-base-url") { + updateParams.APIBaseURL = &endpointAPIBaseURL + } + + newEndpointUpdateReq := apiClientEndpoints.NewUpdateGiteaEndpointParams() + newEndpointUpdateReq.Name = args[0] + newEndpointUpdateReq.Body = updateParams + + response, err := apiCli.Endpoints.UpdateGiteaEndpoint(newEndpointUpdateReq, authToken) + if err != nil { + return err + } + formatOneEndpoint(response.Payload) + return nil + }, +} + +func init() { + giteaEndpointCreateCmd.Flags().StringVar(&endpointName, "name", "", "Name of the Gitea endpoint") + giteaEndpointCreateCmd.Flags().StringVar(&endpointDescription, "description", "", "Description for the github endpoint") + giteaEndpointCreateCmd.Flags().StringVar(&endpointBaseURL, "base-url", "", "Base URL of the Gitea endpoint") + giteaEndpointCreateCmd.Flags().StringVar(&endpointAPIBaseURL, "api-base-url", "", "API Base URL of the Gitea endpoint") + giteaEndpointCreateCmd.Flags().StringVar(&endpointCACertPath, "ca-cert-path", "", "CA Cert Path of the Gitea endpoint") + + giteaEndpointListCmd.Flags().BoolVarP(&long, "long", "l", false, "Include additional info.") + + giteaEndpointCreateCmd.MarkFlagRequired("name") + giteaEndpointCreateCmd.MarkFlagRequired("base-url") + giteaEndpointCreateCmd.MarkFlagRequired("api-base-url") + + giteaEndpointUpdateCmd.Flags().StringVar(&endpointDescription, "description", "", "Description for the gitea endpoint") + giteaEndpointUpdateCmd.Flags().StringVar(&endpointBaseURL, "base-url", "", "Base URL of the Gitea endpoint") + giteaEndpointUpdateCmd.Flags().StringVar(&endpointAPIBaseURL, "api-base-url", "", "API Base URL of the Gitea endpoint") + giteaEndpointUpdateCmd.Flags().StringVar(&endpointCACertPath, "ca-cert-path", "", "CA Cert Path of the Gitea endpoint") + + giteaEndpointCmd.AddCommand( + giteaEndpointListCmd, + giteaEndpointShowCmd, + giteaEndpointCreateCmd, + giteaEndpointDeleteCmd, + giteaEndpointUpdateCmd, + ) + + giteaCmd.AddCommand(giteaEndpointCmd) +} + +func parseGiteaCreateParams() (params.CreateGiteaEndpointParams, error) { + certBundleBytes, err := parseAndReadCABundle() + if err != nil { + return params.CreateGiteaEndpointParams{}, err + } + + ret := params.CreateGiteaEndpointParams{ + Name: endpointName, + BaseURL: endpointBaseURL, + APIBaseURL: endpointAPIBaseURL, + Description: endpointDescription, + CACertBundle: certBundleBytes, + } + return ret, nil +} diff --git a/cmd/garm-cli/cmd/github.go b/cmd/garm-cli/cmd/github.go index 8b79a3819..71342026c 100644 --- a/cmd/garm-cli/cmd/github.go +++ b/cmd/garm-cli/cmd/github.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package cmd import "github.com/spf13/cobra" diff --git a/cmd/garm-cli/cmd/github_credentials.go b/cmd/garm-cli/cmd/github_credentials.go index 2b2128d08..ae2374f62 100644 --- a/cmd/garm-cli/cmd/github_credentials.go +++ b/cmd/garm-cli/cmd/github_credentials.go @@ -283,12 +283,12 @@ func parsePrivateKeyFromPath(path string) ([]byte, error) { func parseCredentialsAddParams() (ret params.CreateGithubCredentialsParams, err error) { ret.Name = credentialsName ret.Description = credentialsDescription - ret.AuthType = params.GithubAuthType(credentialsType) + ret.AuthType = params.ForgeAuthType(credentialsType) ret.Endpoint = credentialsEndpoint switch ret.AuthType { - case params.GithubAuthTypePAT: + case params.ForgeAuthTypePAT: ret.PAT.OAuth2Token = credentialsOAuthToken - case params.GithubAuthTypeApp: + case params.ForgeAuthTypeApp: ret.App.InstallationID = credentialsAppInstallationID ret.App.AppID = credentialsAppID keyContents, err := parsePrivateKeyFromPath(credentialsPrivateKeyPath) @@ -344,7 +344,7 @@ func parseCredentialsUpdateParams() (params.UpdateGithubCredentialsParams, error return updateParams, nil } -func formatGithubCredentials(creds []params.GithubCredentials) { +func formatGithubCredentials(creds []params.ForgeCredentials) { if outputFormat == common.OutputFormatJSON { printAsJSON(creds) return @@ -366,7 +366,7 @@ func formatGithubCredentials(creds []params.GithubCredentials) { fmt.Println(t.Render()) } -func formatOneGithubCredential(cred params.GithubCredentials) { +func formatOneGithubCredential(cred params.ForgeCredentials) { if outputFormat == common.OutputFormatJSON { printAsJSON(cred) return diff --git a/cmd/garm-cli/cmd/github_endpoints.go b/cmd/garm-cli/cmd/github_endpoints.go index 2be14f520..61f468109 100644 --- a/cmd/garm-cli/cmd/github_endpoints.go +++ b/cmd/garm-cli/cmd/github_endpoints.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package cmd import ( @@ -145,7 +158,7 @@ var githubEndpointUpdateCmd = &cobra.Command{ updateParams := params.UpdateGithubEndpointParams{} if cmd.Flags().Changed("ca-cert-path") { - cert, err := parseReadAndParsCABundle() + cert, err := parseAndReadCABundle() if err != nil { return err } @@ -213,7 +226,7 @@ func init() { githubCmd.AddCommand(githubEndpointCmd) } -func parseReadAndParsCABundle() ([]byte, error) { +func parseAndReadCABundle() ([]byte, error) { if endpointCACertPath == "" { return nil, nil } @@ -236,7 +249,7 @@ func parseReadAndParsCABundle() ([]byte, error) { } func parseCreateParams() (params.CreateGithubEndpointParams, error) { - certBundleBytes, err := parseReadAndParsCABundle() + certBundleBytes, err := parseAndReadCABundle() if err != nil { return params.CreateGithubEndpointParams{}, err } @@ -252,7 +265,7 @@ func parseCreateParams() (params.CreateGithubEndpointParams, error) { return ret, nil } -func formatEndpoints(endpoints params.GithubEndpoints) { +func formatEndpoints(endpoints params.ForgeEndpoints) { if outputFormat == common.OutputFormatJSON { printAsJSON(endpoints) return @@ -274,7 +287,7 @@ func formatEndpoints(endpoints params.GithubEndpoints) { fmt.Println(t.Render()) } -func formatOneEndpoint(endpoint params.GithubEndpoint) { +func formatOneEndpoint(endpoint params.ForgeEndpoint) { if outputFormat == common.OutputFormatJSON { printAsJSON(endpoint) return @@ -287,7 +300,9 @@ func formatOneEndpoint(endpoint params.GithubEndpoint) { t.AppendRow([]interface{}{"Created At", endpoint.CreatedAt}) t.AppendRow([]interface{}{"Updated At", endpoint.UpdatedAt}) t.AppendRow([]interface{}{"Base URL", endpoint.BaseURL}) - t.AppendRow([]interface{}{"Upload URL", endpoint.UploadBaseURL}) + if endpoint.UploadBaseURL != "" { + t.AppendRow([]interface{}{"Upload URL", endpoint.UploadBaseURL}) + } t.AppendRow([]interface{}{"API Base URL", endpoint.APIBaseURL}) if len(endpoint.CACertBundle) > 0 { t.AppendRow([]interface{}{"CA Cert Bundle", string(endpoint.CACertBundle)}) diff --git a/cmd/garm-cli/cmd/log.go b/cmd/garm-cli/cmd/log.go index 901e8e0fd..e930ae695 100644 --- a/cmd/garm-cli/cmd/log.go +++ b/cmd/garm-cli/cmd/log.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package cmd import ( diff --git a/cmd/garm-cli/cmd/organization.go b/cmd/garm-cli/cmd/organization.go index c7b80feca..c35fd75b8 100644 --- a/cmd/garm-cli/cmd/organization.go +++ b/cmd/garm-cli/cmd/organization.go @@ -167,6 +167,7 @@ var orgAddCmd = &cobra.Command{ Name: orgName, WebhookSecret: orgWebhookSecret, CredentialsName: orgCreds, + ForgeType: params.EndpointType(forgeType), PoolBalancerType: params.PoolBalancerType(poolBalancerType), } response, err := apiCli.Organizations.CreateOrg(newOrgReq, authToken) @@ -306,6 +307,7 @@ func init() { orgAddCmd.Flags().StringVar(&orgName, "name", "", "The name of the organization") orgAddCmd.Flags().StringVar(&poolBalancerType, "pool-balancer-type", string(params.PoolBalancerTypeRoundRobin), "The balancing strategy to use when creating runners in pools matching requested labels.") orgAddCmd.Flags().StringVar(&orgWebhookSecret, "webhook-secret", "", "The webhook secret for this organization") + orgAddCmd.Flags().StringVar(&forgeType, "forge-type", string(params.GithubEndpointType), "The forge type of the organization. Supported values: github, gitea.") orgAddCmd.Flags().StringVar(&orgCreds, "credentials", "", "Credentials name. See credentials list.") orgAddCmd.Flags().BoolVar(&orgRandomWebhookSecret, "random-webhook-secret", false, "Generate a random webhook secret for this organization.") orgAddCmd.Flags().BoolVar(&installOrgWebhook, "install-webhook", false, "Install the webhook as part of the add operation.") @@ -347,13 +349,17 @@ func formatOrganizations(orgs []params.Organization) { return } t := table.NewWriter() - header := table.Row{"ID", "Name", "Endpoint", "Credentials name", "Pool Balancer Type", "Pool mgr running"} + header := table.Row{"ID", "Name", "Endpoint", "Credentials name", "Pool Balancer Type", "Forge type", "Pool mgr running"} if long { header = append(header, "Created At", "Updated At") } t.AppendHeader(header) for _, val := range orgs { - row := table.Row{val.ID, val.Name, val.Endpoint.Name, val.CredentialsName, val.GetBalancerType(), val.PoolManagerStatus.IsRunning} + forgeType := val.Endpoint.EndpointType + if forgeType == "" { + forgeType = params.GithubEndpointType + } + row := table.Row{val.ID, val.Name, val.Endpoint.Name, val.CredentialsName, val.GetBalancerType(), forgeType, val.PoolManagerStatus.IsRunning} if long { row = append(row, val.CreatedAt, val.UpdatedAt) } diff --git a/cmd/garm-cli/cmd/repository.go b/cmd/garm-cli/cmd/repository.go index 1c453836e..c94495cda 100644 --- a/cmd/garm-cli/cmd/repository.go +++ b/cmd/garm-cli/cmd/repository.go @@ -31,6 +31,7 @@ var ( repoName string repoWebhookSecret string repoCreds string + forgeType string randomWebhookSecret bool insecureRepoWebhook bool keepRepoWebhook bool @@ -169,6 +170,7 @@ var repoAddCmd = &cobra.Command{ Name: repoName, WebhookSecret: repoWebhookSecret, CredentialsName: repoCreds, + ForgeType: params.EndpointType(forgeType), PoolBalancerType: params.PoolBalancerType(poolBalancerType), } response, err := apiCli.Repositories.CreateRepo(newRepoReq, authToken) @@ -309,6 +311,7 @@ func init() { repoAddCmd.Flags().StringVar(&repoOwner, "owner", "", "The owner of this repository") repoAddCmd.Flags().StringVar(&poolBalancerType, "pool-balancer-type", string(params.PoolBalancerTypeRoundRobin), "The balancing strategy to use when creating runners in pools matching requested labels.") repoAddCmd.Flags().StringVar(&repoName, "name", "", "The name of the repository") + repoAddCmd.Flags().StringVar(&forgeType, "forge-type", string(params.GithubEndpointType), "The forge type of the repository. Supported values: github, gitea.") repoAddCmd.Flags().StringVar(&repoWebhookSecret, "webhook-secret", "", "The webhook secret for this repository") repoAddCmd.Flags().StringVar(&repoCreds, "credentials", "", "Credentials name. See credentials list.") repoAddCmd.Flags().BoolVar(&randomWebhookSecret, "random-webhook-secret", false, "Generate a random webhook secret for this repository.") @@ -354,13 +357,17 @@ func formatRepositories(repos []params.Repository) { return } t := table.NewWriter() - header := table.Row{"ID", "Owner", "Name", "Endpoint", "Credentials name", "Pool Balancer Type", "Pool mgr running"} + header := table.Row{"ID", "Owner", "Name", "Endpoint", "Credentials name", "Pool Balancer Type", "Forge type", "Pool mgr running"} if long { header = append(header, "Created At", "Updated At") } t.AppendHeader(header) for _, val := range repos { - row := table.Row{val.ID, val.Owner, val.Name, val.Endpoint.Name, val.CredentialsName, val.GetBalancerType(), val.PoolManagerStatus.IsRunning} + forgeType := val.Endpoint.EndpointType + if forgeType == "" { + forgeType = params.GithubEndpointType + } + row := table.Row{val.ID, val.Owner, val.Name, val.Endpoint.Name, val.GetCredentialsName(), val.GetBalancerType(), forgeType, val.PoolManagerStatus.IsRunning} if long { row = append(row, val.CreatedAt, val.UpdatedAt) } @@ -386,7 +393,7 @@ func formatOneRepository(repo params.Repository) { t.AppendRow(table.Row{"Name", repo.Name}) t.AppendRow(table.Row{"Endpoint", repo.Endpoint.Name}) t.AppendRow(table.Row{"Pool balancer type", repo.GetBalancerType()}) - t.AppendRow(table.Row{"Credentials", repo.CredentialsName}) + t.AppendRow(table.Row{"Credentials", repo.GetCredentialsName()}) t.AppendRow(table.Row{"Pool manager running", repo.PoolManagerStatus.IsRunning}) if !repo.PoolManagerStatus.IsRunning { t.AppendRow(table.Row{"Failure reason", repo.PoolManagerStatus.FailureReason}) diff --git a/cmd/garm-cli/common/cobra.go b/cmd/garm-cli/common/cobra.go index e59a2aca9..399a4b921 100644 --- a/cmd/garm-cli/common/cobra.go +++ b/cmd/garm-cli/common/cobra.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package common import "fmt" diff --git a/cmd/garm-cli/config/home_nix.go b/cmd/garm-cli/config/home_nix.go index 27aed4f84..e9ffa521c 100644 --- a/cmd/garm-cli/config/home_nix.go +++ b/cmd/garm-cli/config/home_nix.go @@ -1,6 +1,19 @@ //go:build !windows // +build !windows +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package config import ( diff --git a/cmd/garm-cli/config/home_windows.go b/cmd/garm-cli/config/home_windows.go index d34379b43..c70fb6454 100644 --- a/cmd/garm-cli/config/home_windows.go +++ b/cmd/garm-cli/config/home_windows.go @@ -1,6 +1,19 @@ //go:build windows && !linux // +build windows,!linux +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package config import ( diff --git a/cmd/garm/main.go b/cmd/garm/main.go index 20f34eba4..f0cca079a 100644 --- a/cmd/garm/main.go +++ b/cmd/garm/main.go @@ -240,10 +240,10 @@ func main() { cacheWorker := cache.NewWorker(ctx, db) if err != nil { - log.Fatalf("failed to create credentials worker: %+v", err) + log.Fatalf("failed to create cache worker: %+v", err) } if err := cacheWorker.Start(); err != nil { - log.Fatalf("failed to start credentials worker: %+v", err) + log.Fatalf("failed to start cache worker: %+v", err) } providers, err := providers.LoadProvidersFromConfig(ctx, *cfg, controllerInfo.ControllerID.String()) @@ -384,6 +384,7 @@ func main() { slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to stop provider worker") } + slog.InfoContext(ctx, "shutting down http server") shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 60*time.Second) defer shutdownCancel() if err := srv.Shutdown(shutdownCtx); err != nil { diff --git a/database/common/errors.go b/database/common/errors.go index df2d936a6..5e6a50873 100644 --- a/database/common/errors.go +++ b/database/common/errors.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package common import "fmt" diff --git a/database/common/mocks/Store.go b/database/common/mocks/Store.go index f7f508b5a..c5994b87c 100644 --- a/database/common/mocks/Store.go +++ b/database/common/mocks/Store.go @@ -15,7 +15,7 @@ type Store struct { } // AddEntityEvent provides a mock function with given fields: ctx, entity, event, eventLevel, statusMessage, maxEvents -func (_m *Store) AddEntityEvent(ctx context.Context, entity params.GithubEntity, event params.EventType, eventLevel params.EventLevel, statusMessage string, maxEvents int) error { +func (_m *Store) AddEntityEvent(ctx context.Context, entity params.ForgeEntity, event params.EventType, eventLevel params.EventLevel, statusMessage string, maxEvents int) error { ret := _m.Called(ctx, entity, event, eventLevel, statusMessage, maxEvents) if len(ret) == 0 { @@ -23,7 +23,7 @@ func (_m *Store) AddEntityEvent(ctx context.Context, entity params.GithubEntity, } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntity, params.EventType, params.EventLevel, string, int) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntity, params.EventType, params.EventLevel, string, int) error); ok { r0 = rf(ctx, entity, event, eventLevel, statusMessage, maxEvents) } else { r0 = ret.Error(0) @@ -125,7 +125,7 @@ func (_m *Store) CreateEnterprise(ctx context.Context, name string, credentialsN } // CreateEntityPool provides a mock function with given fields: ctx, entity, param -func (_m *Store) CreateEntityPool(ctx context.Context, entity params.GithubEntity, param params.CreatePoolParams) (params.Pool, error) { +func (_m *Store) CreateEntityPool(ctx context.Context, entity params.ForgeEntity, param params.CreatePoolParams) (params.Pool, error) { ret := _m.Called(ctx, entity, param) if len(ret) == 0 { @@ -134,16 +134,16 @@ func (_m *Store) CreateEntityPool(ctx context.Context, entity params.GithubEntit var r0 params.Pool var r1 error - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntity, params.CreatePoolParams) (params.Pool, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntity, params.CreatePoolParams) (params.Pool, error)); ok { return rf(ctx, entity, param) } - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntity, params.CreatePoolParams) params.Pool); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntity, params.CreatePoolParams) params.Pool); ok { r0 = rf(ctx, entity, param) } else { r0 = ret.Get(0).(params.Pool) } - if rf, ok := ret.Get(1).(func(context.Context, params.GithubEntity, params.CreatePoolParams) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, params.ForgeEntity, params.CreatePoolParams) error); ok { r1 = rf(ctx, entity, param) } else { r1 = ret.Error(1) @@ -153,7 +153,7 @@ func (_m *Store) CreateEntityPool(ctx context.Context, entity params.GithubEntit } // CreateEntityScaleSet provides a mock function with given fields: _a0, entity, param -func (_m *Store) CreateEntityScaleSet(_a0 context.Context, entity params.GithubEntity, param params.CreateScaleSetParams) (params.ScaleSet, error) { +func (_m *Store) CreateEntityScaleSet(_a0 context.Context, entity params.ForgeEntity, param params.CreateScaleSetParams) (params.ScaleSet, error) { ret := _m.Called(_a0, entity, param) if len(ret) == 0 { @@ -162,16 +162,16 @@ func (_m *Store) CreateEntityScaleSet(_a0 context.Context, entity params.GithubE var r0 params.ScaleSet var r1 error - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntity, params.CreateScaleSetParams) (params.ScaleSet, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntity, params.CreateScaleSetParams) (params.ScaleSet, error)); ok { return rf(_a0, entity, param) } - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntity, params.CreateScaleSetParams) params.ScaleSet); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntity, params.CreateScaleSetParams) params.ScaleSet); ok { r0 = rf(_a0, entity, param) } else { r0 = ret.Get(0).(params.ScaleSet) } - if rf, ok := ret.Get(1).(func(context.Context, params.GithubEntity, params.CreateScaleSetParams) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, params.ForgeEntity, params.CreateScaleSetParams) error); ok { r1 = rf(_a0, entity, param) } else { r1 = ret.Error(1) @@ -180,23 +180,79 @@ func (_m *Store) CreateEntityScaleSet(_a0 context.Context, entity params.GithubE return r0, r1 } +// CreateGiteaCredentials provides a mock function with given fields: ctx, param +func (_m *Store) CreateGiteaCredentials(ctx context.Context, param params.CreateGiteaCredentialsParams) (params.ForgeCredentials, error) { + ret := _m.Called(ctx, param) + + if len(ret) == 0 { + panic("no return value specified for CreateGiteaCredentials") + } + + var r0 params.ForgeCredentials + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, params.CreateGiteaCredentialsParams) (params.ForgeCredentials, error)); ok { + return rf(ctx, param) + } + if rf, ok := ret.Get(0).(func(context.Context, params.CreateGiteaCredentialsParams) params.ForgeCredentials); ok { + r0 = rf(ctx, param) + } else { + r0 = ret.Get(0).(params.ForgeCredentials) + } + + if rf, ok := ret.Get(1).(func(context.Context, params.CreateGiteaCredentialsParams) error); ok { + r1 = rf(ctx, param) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CreateGiteaEndpoint provides a mock function with given fields: _a0, param +func (_m *Store) CreateGiteaEndpoint(_a0 context.Context, param params.CreateGiteaEndpointParams) (params.ForgeEndpoint, error) { + ret := _m.Called(_a0, param) + + if len(ret) == 0 { + panic("no return value specified for CreateGiteaEndpoint") + } + + var r0 params.ForgeEndpoint + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, params.CreateGiteaEndpointParams) (params.ForgeEndpoint, error)); ok { + return rf(_a0, param) + } + if rf, ok := ret.Get(0).(func(context.Context, params.CreateGiteaEndpointParams) params.ForgeEndpoint); ok { + r0 = rf(_a0, param) + } else { + r0 = ret.Get(0).(params.ForgeEndpoint) + } + + if rf, ok := ret.Get(1).(func(context.Context, params.CreateGiteaEndpointParams) error); ok { + r1 = rf(_a0, param) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // CreateGithubCredentials provides a mock function with given fields: ctx, param -func (_m *Store) CreateGithubCredentials(ctx context.Context, param params.CreateGithubCredentialsParams) (params.GithubCredentials, error) { +func (_m *Store) CreateGithubCredentials(ctx context.Context, param params.CreateGithubCredentialsParams) (params.ForgeCredentials, error) { ret := _m.Called(ctx, param) if len(ret) == 0 { panic("no return value specified for CreateGithubCredentials") } - var r0 params.GithubCredentials + var r0 params.ForgeCredentials var r1 error - if rf, ok := ret.Get(0).(func(context.Context, params.CreateGithubCredentialsParams) (params.GithubCredentials, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.CreateGithubCredentialsParams) (params.ForgeCredentials, error)); ok { return rf(ctx, param) } - if rf, ok := ret.Get(0).(func(context.Context, params.CreateGithubCredentialsParams) params.GithubCredentials); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.CreateGithubCredentialsParams) params.ForgeCredentials); ok { r0 = rf(ctx, param) } else { - r0 = ret.Get(0).(params.GithubCredentials) + r0 = ret.Get(0).(params.ForgeCredentials) } if rf, ok := ret.Get(1).(func(context.Context, params.CreateGithubCredentialsParams) error); ok { @@ -209,22 +265,22 @@ func (_m *Store) CreateGithubCredentials(ctx context.Context, param params.Creat } // CreateGithubEndpoint provides a mock function with given fields: ctx, param -func (_m *Store) CreateGithubEndpoint(ctx context.Context, param params.CreateGithubEndpointParams) (params.GithubEndpoint, error) { +func (_m *Store) CreateGithubEndpoint(ctx context.Context, param params.CreateGithubEndpointParams) (params.ForgeEndpoint, error) { ret := _m.Called(ctx, param) if len(ret) == 0 { panic("no return value specified for CreateGithubEndpoint") } - var r0 params.GithubEndpoint + var r0 params.ForgeEndpoint var r1 error - if rf, ok := ret.Get(0).(func(context.Context, params.CreateGithubEndpointParams) (params.GithubEndpoint, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.CreateGithubEndpointParams) (params.ForgeEndpoint, error)); ok { return rf(ctx, param) } - if rf, ok := ret.Get(0).(func(context.Context, params.CreateGithubEndpointParams) params.GithubEndpoint); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.CreateGithubEndpointParams) params.ForgeEndpoint); ok { r0 = rf(ctx, param) } else { - r0 = ret.Get(0).(params.GithubEndpoint) + r0 = ret.Get(0).(params.ForgeEndpoint) } if rf, ok := ret.Get(1).(func(context.Context, params.CreateGithubEndpointParams) error); ok { @@ -292,9 +348,9 @@ func (_m *Store) CreateOrUpdateJob(ctx context.Context, job params.Job) (params. return r0, r1 } -// CreateOrganization provides a mock function with given fields: ctx, name, credentialsName, webhookSecret, poolBalancerType -func (_m *Store) CreateOrganization(ctx context.Context, name string, credentialsName string, webhookSecret string, poolBalancerType params.PoolBalancerType) (params.Organization, error) { - ret := _m.Called(ctx, name, credentialsName, webhookSecret, poolBalancerType) +// CreateOrganization provides a mock function with given fields: ctx, name, credentials, webhookSecret, poolBalancerType +func (_m *Store) CreateOrganization(ctx context.Context, name string, credentials params.ForgeCredentials, webhookSecret string, poolBalancerType params.PoolBalancerType) (params.Organization, error) { + ret := _m.Called(ctx, name, credentials, webhookSecret, poolBalancerType) if len(ret) == 0 { panic("no return value specified for CreateOrganization") @@ -302,17 +358,17 @@ func (_m *Store) CreateOrganization(ctx context.Context, name string, credential var r0 params.Organization var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, params.PoolBalancerType) (params.Organization, error)); ok { - return rf(ctx, name, credentialsName, webhookSecret, poolBalancerType) + if rf, ok := ret.Get(0).(func(context.Context, string, params.ForgeCredentials, string, params.PoolBalancerType) (params.Organization, error)); ok { + return rf(ctx, name, credentials, webhookSecret, poolBalancerType) } - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, params.PoolBalancerType) params.Organization); ok { - r0 = rf(ctx, name, credentialsName, webhookSecret, poolBalancerType) + if rf, ok := ret.Get(0).(func(context.Context, string, params.ForgeCredentials, string, params.PoolBalancerType) params.Organization); ok { + r0 = rf(ctx, name, credentials, webhookSecret, poolBalancerType) } else { r0 = ret.Get(0).(params.Organization) } - if rf, ok := ret.Get(1).(func(context.Context, string, string, string, params.PoolBalancerType) error); ok { - r1 = rf(ctx, name, credentialsName, webhookSecret, poolBalancerType) + if rf, ok := ret.Get(1).(func(context.Context, string, params.ForgeCredentials, string, params.PoolBalancerType) error); ok { + r1 = rf(ctx, name, credentials, webhookSecret, poolBalancerType) } else { r1 = ret.Error(1) } @@ -320,9 +376,9 @@ func (_m *Store) CreateOrganization(ctx context.Context, name string, credential return r0, r1 } -// CreateRepository provides a mock function with given fields: ctx, owner, name, credentialsName, webhookSecret, poolBalancerType -func (_m *Store) CreateRepository(ctx context.Context, owner string, name string, credentialsName string, webhookSecret string, poolBalancerType params.PoolBalancerType) (params.Repository, error) { - ret := _m.Called(ctx, owner, name, credentialsName, webhookSecret, poolBalancerType) +// CreateRepository provides a mock function with given fields: ctx, owner, name, credentials, webhookSecret, poolBalancerType +func (_m *Store) CreateRepository(ctx context.Context, owner string, name string, credentials params.ForgeCredentials, webhookSecret string, poolBalancerType params.PoolBalancerType) (params.Repository, error) { + ret := _m.Called(ctx, owner, name, credentials, webhookSecret, poolBalancerType) if len(ret) == 0 { panic("no return value specified for CreateRepository") @@ -330,17 +386,17 @@ func (_m *Store) CreateRepository(ctx context.Context, owner string, name string var r0 params.Repository var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, params.PoolBalancerType) (params.Repository, error)); ok { - return rf(ctx, owner, name, credentialsName, webhookSecret, poolBalancerType) + if rf, ok := ret.Get(0).(func(context.Context, string, string, params.ForgeCredentials, string, params.PoolBalancerType) (params.Repository, error)); ok { + return rf(ctx, owner, name, credentials, webhookSecret, poolBalancerType) } - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, params.PoolBalancerType) params.Repository); ok { - r0 = rf(ctx, owner, name, credentialsName, webhookSecret, poolBalancerType) + if rf, ok := ret.Get(0).(func(context.Context, string, string, params.ForgeCredentials, string, params.PoolBalancerType) params.Repository); ok { + r0 = rf(ctx, owner, name, credentials, webhookSecret, poolBalancerType) } else { r0 = ret.Get(0).(params.Repository) } - if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string, params.PoolBalancerType) error); ok { - r1 = rf(ctx, owner, name, credentialsName, webhookSecret, poolBalancerType) + if rf, ok := ret.Get(1).(func(context.Context, string, string, params.ForgeCredentials, string, params.PoolBalancerType) error); ok { + r1 = rf(ctx, owner, name, credentials, webhookSecret, poolBalancerType) } else { r1 = ret.Error(1) } @@ -441,7 +497,7 @@ func (_m *Store) DeleteEnterprise(ctx context.Context, enterpriseID string) erro } // DeleteEntityPool provides a mock function with given fields: ctx, entity, poolID -func (_m *Store) DeleteEntityPool(ctx context.Context, entity params.GithubEntity, poolID string) error { +func (_m *Store) DeleteEntityPool(ctx context.Context, entity params.ForgeEntity, poolID string) error { ret := _m.Called(ctx, entity, poolID) if len(ret) == 0 { @@ -449,7 +505,7 @@ func (_m *Store) DeleteEntityPool(ctx context.Context, entity params.GithubEntit } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntity, string) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntity, string) error); ok { r0 = rf(ctx, entity, poolID) } else { r0 = ret.Error(0) @@ -458,6 +514,42 @@ func (_m *Store) DeleteEntityPool(ctx context.Context, entity params.GithubEntit return r0 } +// DeleteGiteaCredentials provides a mock function with given fields: ctx, id +func (_m *Store) DeleteGiteaCredentials(ctx context.Context, id uint) error { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for DeleteGiteaCredentials") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, uint) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteGiteaEndpoint provides a mock function with given fields: _a0, name +func (_m *Store) DeleteGiteaEndpoint(_a0 context.Context, name string) error { + ret := _m.Called(_a0, name) + + if len(ret) == 0 { + panic("no return value specified for DeleteGiteaEndpoint") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(_a0, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // DeleteGithubCredentials provides a mock function with given fields: ctx, id func (_m *Store) DeleteGithubCredentials(ctx context.Context, id uint) error { ret := _m.Called(ctx, id) @@ -621,7 +713,7 @@ func (_m *Store) DeleteScaleSetByID(ctx context.Context, scaleSetID uint) error } // FindPoolsMatchingAllTags provides a mock function with given fields: ctx, entityType, entityID, tags -func (_m *Store) FindPoolsMatchingAllTags(ctx context.Context, entityType params.GithubEntityType, entityID string, tags []string) ([]params.Pool, error) { +func (_m *Store) FindPoolsMatchingAllTags(ctx context.Context, entityType params.ForgeEntityType, entityID string, tags []string) ([]params.Pool, error) { ret := _m.Called(ctx, entityType, entityID, tags) if len(ret) == 0 { @@ -630,10 +722,10 @@ func (_m *Store) FindPoolsMatchingAllTags(ctx context.Context, entityType params var r0 []params.Pool var r1 error - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntityType, string, []string) ([]params.Pool, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntityType, string, []string) ([]params.Pool, error)); ok { return rf(ctx, entityType, entityID, tags) } - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntityType, string, []string) []params.Pool); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntityType, string, []string) []params.Pool); ok { r0 = rf(ctx, entityType, entityID, tags) } else { if ret.Get(0) != nil { @@ -641,7 +733,7 @@ func (_m *Store) FindPoolsMatchingAllTags(ctx context.Context, entityType params } } - if rf, ok := ret.Get(1).(func(context.Context, params.GithubEntityType, string, []string) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, params.ForgeEntityType, string, []string) error); ok { r1 = rf(ctx, entityType, entityID, tags) } else { r1 = ret.Error(1) @@ -735,7 +827,7 @@ func (_m *Store) GetEnterpriseByID(ctx context.Context, enterpriseID string) (pa } // GetEntityPool provides a mock function with given fields: ctx, entity, poolID -func (_m *Store) GetEntityPool(ctx context.Context, entity params.GithubEntity, poolID string) (params.Pool, error) { +func (_m *Store) GetEntityPool(ctx context.Context, entity params.ForgeEntity, poolID string) (params.Pool, error) { ret := _m.Called(ctx, entity, poolID) if len(ret) == 0 { @@ -744,16 +836,16 @@ func (_m *Store) GetEntityPool(ctx context.Context, entity params.GithubEntity, var r0 params.Pool var r1 error - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntity, string) (params.Pool, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntity, string) (params.Pool, error)); ok { return rf(ctx, entity, poolID) } - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntity, string) params.Pool); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntity, string) params.Pool); ok { r0 = rf(ctx, entity, poolID) } else { r0 = ret.Get(0).(params.Pool) } - if rf, ok := ret.Get(1).(func(context.Context, params.GithubEntity, string) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, params.ForgeEntity, string) error); ok { r1 = rf(ctx, entity, poolID) } else { r1 = ret.Error(1) @@ -762,23 +854,51 @@ func (_m *Store) GetEntityPool(ctx context.Context, entity params.GithubEntity, return r0, r1 } -// GetGithubCredentials provides a mock function with given fields: ctx, id, detailed -func (_m *Store) GetGithubCredentials(ctx context.Context, id uint, detailed bool) (params.GithubCredentials, error) { +// GetForgeEntity provides a mock function with given fields: _a0, entityType, entityID +func (_m *Store) GetForgeEntity(_a0 context.Context, entityType params.ForgeEntityType, entityID string) (params.ForgeEntity, error) { + ret := _m.Called(_a0, entityType, entityID) + + if len(ret) == 0 { + panic("no return value specified for GetForgeEntity") + } + + var r0 params.ForgeEntity + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntityType, string) (params.ForgeEntity, error)); ok { + return rf(_a0, entityType, entityID) + } + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntityType, string) params.ForgeEntity); ok { + r0 = rf(_a0, entityType, entityID) + } else { + r0 = ret.Get(0).(params.ForgeEntity) + } + + if rf, ok := ret.Get(1).(func(context.Context, params.ForgeEntityType, string) error); ok { + r1 = rf(_a0, entityType, entityID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetGiteaCredentials provides a mock function with given fields: ctx, id, detailed +func (_m *Store) GetGiteaCredentials(ctx context.Context, id uint, detailed bool) (params.ForgeCredentials, error) { ret := _m.Called(ctx, id, detailed) if len(ret) == 0 { - panic("no return value specified for GetGithubCredentials") + panic("no return value specified for GetGiteaCredentials") } - var r0 params.GithubCredentials + var r0 params.ForgeCredentials var r1 error - if rf, ok := ret.Get(0).(func(context.Context, uint, bool) (params.GithubCredentials, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, uint, bool) (params.ForgeCredentials, error)); ok { return rf(ctx, id, detailed) } - if rf, ok := ret.Get(0).(func(context.Context, uint, bool) params.GithubCredentials); ok { + if rf, ok := ret.Get(0).(func(context.Context, uint, bool) params.ForgeCredentials); ok { r0 = rf(ctx, id, detailed) } else { - r0 = ret.Get(0).(params.GithubCredentials) + r0 = ret.Get(0).(params.ForgeCredentials) } if rf, ok := ret.Get(1).(func(context.Context, uint, bool) error); ok { @@ -790,23 +910,23 @@ func (_m *Store) GetGithubCredentials(ctx context.Context, id uint, detailed boo return r0, r1 } -// GetGithubCredentialsByName provides a mock function with given fields: ctx, name, detailed -func (_m *Store) GetGithubCredentialsByName(ctx context.Context, name string, detailed bool) (params.GithubCredentials, error) { +// GetGiteaCredentialsByName provides a mock function with given fields: ctx, name, detailed +func (_m *Store) GetGiteaCredentialsByName(ctx context.Context, name string, detailed bool) (params.ForgeCredentials, error) { ret := _m.Called(ctx, name, detailed) if len(ret) == 0 { - panic("no return value specified for GetGithubCredentialsByName") + panic("no return value specified for GetGiteaCredentialsByName") } - var r0 params.GithubCredentials + var r0 params.ForgeCredentials var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, bool) (params.GithubCredentials, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, bool) (params.ForgeCredentials, error)); ok { return rf(ctx, name, detailed) } - if rf, ok := ret.Get(0).(func(context.Context, string, bool) params.GithubCredentials); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, bool) params.ForgeCredentials); ok { r0 = rf(ctx, name, detailed) } else { - r0 = ret.Get(0).(params.GithubCredentials) + r0 = ret.Get(0).(params.ForgeCredentials) } if rf, ok := ret.Get(1).(func(context.Context, string, bool) error); ok { @@ -818,27 +938,27 @@ func (_m *Store) GetGithubCredentialsByName(ctx context.Context, name string, de return r0, r1 } -// GetGithubEndpoint provides a mock function with given fields: ctx, name -func (_m *Store) GetGithubEndpoint(ctx context.Context, name string) (params.GithubEndpoint, error) { - ret := _m.Called(ctx, name) +// GetGiteaEndpoint provides a mock function with given fields: _a0, name +func (_m *Store) GetGiteaEndpoint(_a0 context.Context, name string) (params.ForgeEndpoint, error) { + ret := _m.Called(_a0, name) if len(ret) == 0 { - panic("no return value specified for GetGithubEndpoint") + panic("no return value specified for GetGiteaEndpoint") } - var r0 params.GithubEndpoint + var r0 params.ForgeEndpoint var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (params.GithubEndpoint, error)); ok { - return rf(ctx, name) + if rf, ok := ret.Get(0).(func(context.Context, string) (params.ForgeEndpoint, error)); ok { + return rf(_a0, name) } - if rf, ok := ret.Get(0).(func(context.Context, string) params.GithubEndpoint); ok { - r0 = rf(ctx, name) + if rf, ok := ret.Get(0).(func(context.Context, string) params.ForgeEndpoint); ok { + r0 = rf(_a0, name) } else { - r0 = ret.Get(0).(params.GithubEndpoint) + r0 = ret.Get(0).(params.ForgeEndpoint) } if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, name) + r1 = rf(_a0, name) } else { r1 = ret.Error(1) } @@ -846,27 +966,83 @@ func (_m *Store) GetGithubEndpoint(ctx context.Context, name string) (params.Git return r0, r1 } -// GetGithubEntity provides a mock function with given fields: _a0, entityType, entityID -func (_m *Store) GetGithubEntity(_a0 context.Context, entityType params.GithubEntityType, entityID string) (params.GithubEntity, error) { - ret := _m.Called(_a0, entityType, entityID) +// GetGithubCredentials provides a mock function with given fields: ctx, id, detailed +func (_m *Store) GetGithubCredentials(ctx context.Context, id uint, detailed bool) (params.ForgeCredentials, error) { + ret := _m.Called(ctx, id, detailed) if len(ret) == 0 { - panic("no return value specified for GetGithubEntity") + panic("no return value specified for GetGithubCredentials") } - var r0 params.GithubEntity + var r0 params.ForgeCredentials var r1 error - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntityType, string) (params.GithubEntity, error)); ok { - return rf(_a0, entityType, entityID) + if rf, ok := ret.Get(0).(func(context.Context, uint, bool) (params.ForgeCredentials, error)); ok { + return rf(ctx, id, detailed) } - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntityType, string) params.GithubEntity); ok { - r0 = rf(_a0, entityType, entityID) + if rf, ok := ret.Get(0).(func(context.Context, uint, bool) params.ForgeCredentials); ok { + r0 = rf(ctx, id, detailed) } else { - r0 = ret.Get(0).(params.GithubEntity) + r0 = ret.Get(0).(params.ForgeCredentials) } - if rf, ok := ret.Get(1).(func(context.Context, params.GithubEntityType, string) error); ok { - r1 = rf(_a0, entityType, entityID) + if rf, ok := ret.Get(1).(func(context.Context, uint, bool) error); ok { + r1 = rf(ctx, id, detailed) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetGithubCredentialsByName provides a mock function with given fields: ctx, name, detailed +func (_m *Store) GetGithubCredentialsByName(ctx context.Context, name string, detailed bool) (params.ForgeCredentials, error) { + ret := _m.Called(ctx, name, detailed) + + if len(ret) == 0 { + panic("no return value specified for GetGithubCredentialsByName") + } + + var r0 params.ForgeCredentials + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, bool) (params.ForgeCredentials, error)); ok { + return rf(ctx, name, detailed) + } + if rf, ok := ret.Get(0).(func(context.Context, string, bool) params.ForgeCredentials); ok { + r0 = rf(ctx, name, detailed) + } else { + r0 = ret.Get(0).(params.ForgeCredentials) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, bool) error); ok { + r1 = rf(ctx, name, detailed) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetGithubEndpoint provides a mock function with given fields: ctx, name +func (_m *Store) GetGithubEndpoint(ctx context.Context, name string) (params.ForgeEndpoint, error) { + ret := _m.Called(ctx, name) + + if len(ret) == 0 { + panic("no return value specified for GetGithubEndpoint") + } + + var r0 params.ForgeEndpoint + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (params.ForgeEndpoint, error)); ok { + return rf(ctx, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string) params.ForgeEndpoint); ok { + r0 = rf(ctx, name) + } else { + r0 = ret.Get(0).(params.ForgeEndpoint) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, name) } else { r1 = ret.Error(1) } @@ -1379,7 +1555,7 @@ func (_m *Store) ListEnterprises(ctx context.Context) ([]params.Enterprise, erro } // ListEntityInstances provides a mock function with given fields: ctx, entity -func (_m *Store) ListEntityInstances(ctx context.Context, entity params.GithubEntity) ([]params.Instance, error) { +func (_m *Store) ListEntityInstances(ctx context.Context, entity params.ForgeEntity) ([]params.Instance, error) { ret := _m.Called(ctx, entity) if len(ret) == 0 { @@ -1388,10 +1564,10 @@ func (_m *Store) ListEntityInstances(ctx context.Context, entity params.GithubEn var r0 []params.Instance var r1 error - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntity) ([]params.Instance, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntity) ([]params.Instance, error)); ok { return rf(ctx, entity) } - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntity) []params.Instance); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntity) []params.Instance); ok { r0 = rf(ctx, entity) } else { if ret.Get(0) != nil { @@ -1399,7 +1575,7 @@ func (_m *Store) ListEntityInstances(ctx context.Context, entity params.GithubEn } } - if rf, ok := ret.Get(1).(func(context.Context, params.GithubEntity) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, params.ForgeEntity) error); ok { r1 = rf(ctx, entity) } else { r1 = ret.Error(1) @@ -1409,7 +1585,7 @@ func (_m *Store) ListEntityInstances(ctx context.Context, entity params.GithubEn } // ListEntityJobsByStatus provides a mock function with given fields: ctx, entityType, entityID, status -func (_m *Store) ListEntityJobsByStatus(ctx context.Context, entityType params.GithubEntityType, entityID string, status params.JobStatus) ([]params.Job, error) { +func (_m *Store) ListEntityJobsByStatus(ctx context.Context, entityType params.ForgeEntityType, entityID string, status params.JobStatus) ([]params.Job, error) { ret := _m.Called(ctx, entityType, entityID, status) if len(ret) == 0 { @@ -1418,10 +1594,10 @@ func (_m *Store) ListEntityJobsByStatus(ctx context.Context, entityType params.G var r0 []params.Job var r1 error - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntityType, string, params.JobStatus) ([]params.Job, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntityType, string, params.JobStatus) ([]params.Job, error)); ok { return rf(ctx, entityType, entityID, status) } - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntityType, string, params.JobStatus) []params.Job); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntityType, string, params.JobStatus) []params.Job); ok { r0 = rf(ctx, entityType, entityID, status) } else { if ret.Get(0) != nil { @@ -1429,7 +1605,7 @@ func (_m *Store) ListEntityJobsByStatus(ctx context.Context, entityType params.G } } - if rf, ok := ret.Get(1).(func(context.Context, params.GithubEntityType, string, params.JobStatus) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, params.ForgeEntityType, string, params.JobStatus) error); ok { r1 = rf(ctx, entityType, entityID, status) } else { r1 = ret.Error(1) @@ -1439,7 +1615,7 @@ func (_m *Store) ListEntityJobsByStatus(ctx context.Context, entityType params.G } // ListEntityPools provides a mock function with given fields: ctx, entity -func (_m *Store) ListEntityPools(ctx context.Context, entity params.GithubEntity) ([]params.Pool, error) { +func (_m *Store) ListEntityPools(ctx context.Context, entity params.ForgeEntity) ([]params.Pool, error) { ret := _m.Called(ctx, entity) if len(ret) == 0 { @@ -1448,10 +1624,10 @@ func (_m *Store) ListEntityPools(ctx context.Context, entity params.GithubEntity var r0 []params.Pool var r1 error - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntity) ([]params.Pool, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntity) ([]params.Pool, error)); ok { return rf(ctx, entity) } - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntity) []params.Pool); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntity) []params.Pool); ok { r0 = rf(ctx, entity) } else { if ret.Get(0) != nil { @@ -1459,7 +1635,7 @@ func (_m *Store) ListEntityPools(ctx context.Context, entity params.GithubEntity } } - if rf, ok := ret.Get(1).(func(context.Context, params.GithubEntity) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, params.ForgeEntity) error); ok { r1 = rf(ctx, entity) } else { r1 = ret.Error(1) @@ -1469,7 +1645,7 @@ func (_m *Store) ListEntityPools(ctx context.Context, entity params.GithubEntity } // ListEntityScaleSets provides a mock function with given fields: _a0, entity -func (_m *Store) ListEntityScaleSets(_a0 context.Context, entity params.GithubEntity) ([]params.ScaleSet, error) { +func (_m *Store) ListEntityScaleSets(_a0 context.Context, entity params.ForgeEntity) ([]params.ScaleSet, error) { ret := _m.Called(_a0, entity) if len(ret) == 0 { @@ -1478,10 +1654,10 @@ func (_m *Store) ListEntityScaleSets(_a0 context.Context, entity params.GithubEn var r0 []params.ScaleSet var r1 error - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntity) ([]params.ScaleSet, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntity) ([]params.ScaleSet, error)); ok { return rf(_a0, entity) } - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntity) []params.ScaleSet); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntity) []params.ScaleSet); ok { r0 = rf(_a0, entity) } else { if ret.Get(0) != nil { @@ -1489,7 +1665,7 @@ func (_m *Store) ListEntityScaleSets(_a0 context.Context, entity params.GithubEn } } - if rf, ok := ret.Get(1).(func(context.Context, params.GithubEntity) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, params.ForgeEntity) error); ok { r1 = rf(_a0, entity) } else { r1 = ret.Error(1) @@ -1498,24 +1674,84 @@ func (_m *Store) ListEntityScaleSets(_a0 context.Context, entity params.GithubEn return r0, r1 } +// ListGiteaCredentials provides a mock function with given fields: ctx +func (_m *Store) ListGiteaCredentials(ctx context.Context) ([]params.ForgeCredentials, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ListGiteaCredentials") + } + + var r0 []params.ForgeCredentials + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]params.ForgeCredentials, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []params.ForgeCredentials); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]params.ForgeCredentials) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListGiteaEndpoints provides a mock function with given fields: _a0 +func (_m *Store) ListGiteaEndpoints(_a0 context.Context) ([]params.ForgeEndpoint, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for ListGiteaEndpoints") + } + + var r0 []params.ForgeEndpoint + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]params.ForgeEndpoint, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(context.Context) []params.ForgeEndpoint); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]params.ForgeEndpoint) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // ListGithubCredentials provides a mock function with given fields: ctx -func (_m *Store) ListGithubCredentials(ctx context.Context) ([]params.GithubCredentials, error) { +func (_m *Store) ListGithubCredentials(ctx context.Context) ([]params.ForgeCredentials, error) { ret := _m.Called(ctx) if len(ret) == 0 { panic("no return value specified for ListGithubCredentials") } - var r0 []params.GithubCredentials + var r0 []params.ForgeCredentials var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]params.GithubCredentials, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context) ([]params.ForgeCredentials, error)); ok { return rf(ctx) } - if rf, ok := ret.Get(0).(func(context.Context) []params.GithubCredentials); ok { + if rf, ok := ret.Get(0).(func(context.Context) []params.ForgeCredentials); ok { r0 = rf(ctx) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]params.GithubCredentials) + r0 = ret.Get(0).([]params.ForgeCredentials) } } @@ -1529,23 +1765,23 @@ func (_m *Store) ListGithubCredentials(ctx context.Context) ([]params.GithubCred } // ListGithubEndpoints provides a mock function with given fields: ctx -func (_m *Store) ListGithubEndpoints(ctx context.Context) ([]params.GithubEndpoint, error) { +func (_m *Store) ListGithubEndpoints(ctx context.Context) ([]params.ForgeEndpoint, error) { ret := _m.Called(ctx) if len(ret) == 0 { panic("no return value specified for ListGithubEndpoints") } - var r0 []params.GithubEndpoint + var r0 []params.ForgeEndpoint var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]params.GithubEndpoint, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context) ([]params.ForgeEndpoint, error)); ok { return rf(ctx) } - if rf, ok := ret.Get(0).(func(context.Context) []params.GithubEndpoint); ok { + if rf, ok := ret.Get(0).(func(context.Context) []params.ForgeEndpoint); ok { r0 = rf(ctx) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]params.GithubEndpoint) + r0 = ret.Get(0).([]params.ForgeEndpoint) } } @@ -1865,7 +2101,7 @@ func (_m *Store) UpdateEnterprise(ctx context.Context, enterpriseID string, para } // UpdateEntityPool provides a mock function with given fields: ctx, entity, poolID, param -func (_m *Store) UpdateEntityPool(ctx context.Context, entity params.GithubEntity, poolID string, param params.UpdatePoolParams) (params.Pool, error) { +func (_m *Store) UpdateEntityPool(ctx context.Context, entity params.ForgeEntity, poolID string, param params.UpdatePoolParams) (params.Pool, error) { ret := _m.Called(ctx, entity, poolID, param) if len(ret) == 0 { @@ -1874,16 +2110,16 @@ func (_m *Store) UpdateEntityPool(ctx context.Context, entity params.GithubEntit var r0 params.Pool var r1 error - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntity, string, params.UpdatePoolParams) (params.Pool, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntity, string, params.UpdatePoolParams) (params.Pool, error)); ok { return rf(ctx, entity, poolID, param) } - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntity, string, params.UpdatePoolParams) params.Pool); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntity, string, params.UpdatePoolParams) params.Pool); ok { r0 = rf(ctx, entity, poolID, param) } else { r0 = ret.Get(0).(params.Pool) } - if rf, ok := ret.Get(1).(func(context.Context, params.GithubEntity, string, params.UpdatePoolParams) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, params.ForgeEntity, string, params.UpdatePoolParams) error); ok { r1 = rf(ctx, entity, poolID, param) } else { r1 = ret.Error(1) @@ -1893,7 +2129,7 @@ func (_m *Store) UpdateEntityPool(ctx context.Context, entity params.GithubEntit } // UpdateEntityScaleSet provides a mock function with given fields: _a0, entity, scaleSetID, param, callback -func (_m *Store) UpdateEntityScaleSet(_a0 context.Context, entity params.GithubEntity, scaleSetID uint, param params.UpdateScaleSetParams, callback func(params.ScaleSet, params.ScaleSet) error) (params.ScaleSet, error) { +func (_m *Store) UpdateEntityScaleSet(_a0 context.Context, entity params.ForgeEntity, scaleSetID uint, param params.UpdateScaleSetParams, callback func(params.ScaleSet, params.ScaleSet) error) (params.ScaleSet, error) { ret := _m.Called(_a0, entity, scaleSetID, param, callback) if len(ret) == 0 { @@ -1902,16 +2138,16 @@ func (_m *Store) UpdateEntityScaleSet(_a0 context.Context, entity params.GithubE var r0 params.ScaleSet var r1 error - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntity, uint, params.UpdateScaleSetParams, func(params.ScaleSet, params.ScaleSet) error) (params.ScaleSet, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntity, uint, params.UpdateScaleSetParams, func(params.ScaleSet, params.ScaleSet) error) (params.ScaleSet, error)); ok { return rf(_a0, entity, scaleSetID, param, callback) } - if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntity, uint, params.UpdateScaleSetParams, func(params.ScaleSet, params.ScaleSet) error) params.ScaleSet); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.ForgeEntity, uint, params.UpdateScaleSetParams, func(params.ScaleSet, params.ScaleSet) error) params.ScaleSet); ok { r0 = rf(_a0, entity, scaleSetID, param, callback) } else { r0 = ret.Get(0).(params.ScaleSet) } - if rf, ok := ret.Get(1).(func(context.Context, params.GithubEntity, uint, params.UpdateScaleSetParams, func(params.ScaleSet, params.ScaleSet) error) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, params.ForgeEntity, uint, params.UpdateScaleSetParams, func(params.ScaleSet, params.ScaleSet) error) error); ok { r1 = rf(_a0, entity, scaleSetID, param, callback) } else { r1 = ret.Error(1) @@ -1920,23 +2156,79 @@ func (_m *Store) UpdateEntityScaleSet(_a0 context.Context, entity params.GithubE return r0, r1 } +// UpdateGiteaCredentials provides a mock function with given fields: ctx, id, param +func (_m *Store) UpdateGiteaCredentials(ctx context.Context, id uint, param params.UpdateGiteaCredentialsParams) (params.ForgeCredentials, error) { + ret := _m.Called(ctx, id, param) + + if len(ret) == 0 { + panic("no return value specified for UpdateGiteaCredentials") + } + + var r0 params.ForgeCredentials + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint, params.UpdateGiteaCredentialsParams) (params.ForgeCredentials, error)); ok { + return rf(ctx, id, param) + } + if rf, ok := ret.Get(0).(func(context.Context, uint, params.UpdateGiteaCredentialsParams) params.ForgeCredentials); ok { + r0 = rf(ctx, id, param) + } else { + r0 = ret.Get(0).(params.ForgeCredentials) + } + + if rf, ok := ret.Get(1).(func(context.Context, uint, params.UpdateGiteaCredentialsParams) error); ok { + r1 = rf(ctx, id, param) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateGiteaEndpoint provides a mock function with given fields: _a0, name, param +func (_m *Store) UpdateGiteaEndpoint(_a0 context.Context, name string, param params.UpdateGiteaEndpointParams) (params.ForgeEndpoint, error) { + ret := _m.Called(_a0, name, param) + + if len(ret) == 0 { + panic("no return value specified for UpdateGiteaEndpoint") + } + + var r0 params.ForgeEndpoint + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, params.UpdateGiteaEndpointParams) (params.ForgeEndpoint, error)); ok { + return rf(_a0, name, param) + } + if rf, ok := ret.Get(0).(func(context.Context, string, params.UpdateGiteaEndpointParams) params.ForgeEndpoint); ok { + r0 = rf(_a0, name, param) + } else { + r0 = ret.Get(0).(params.ForgeEndpoint) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, params.UpdateGiteaEndpointParams) error); ok { + r1 = rf(_a0, name, param) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // UpdateGithubCredentials provides a mock function with given fields: ctx, id, param -func (_m *Store) UpdateGithubCredentials(ctx context.Context, id uint, param params.UpdateGithubCredentialsParams) (params.GithubCredentials, error) { +func (_m *Store) UpdateGithubCredentials(ctx context.Context, id uint, param params.UpdateGithubCredentialsParams) (params.ForgeCredentials, error) { ret := _m.Called(ctx, id, param) if len(ret) == 0 { panic("no return value specified for UpdateGithubCredentials") } - var r0 params.GithubCredentials + var r0 params.ForgeCredentials var r1 error - if rf, ok := ret.Get(0).(func(context.Context, uint, params.UpdateGithubCredentialsParams) (params.GithubCredentials, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, uint, params.UpdateGithubCredentialsParams) (params.ForgeCredentials, error)); ok { return rf(ctx, id, param) } - if rf, ok := ret.Get(0).(func(context.Context, uint, params.UpdateGithubCredentialsParams) params.GithubCredentials); ok { + if rf, ok := ret.Get(0).(func(context.Context, uint, params.UpdateGithubCredentialsParams) params.ForgeCredentials); ok { r0 = rf(ctx, id, param) } else { - r0 = ret.Get(0).(params.GithubCredentials) + r0 = ret.Get(0).(params.ForgeCredentials) } if rf, ok := ret.Get(1).(func(context.Context, uint, params.UpdateGithubCredentialsParams) error); ok { @@ -1949,22 +2241,22 @@ func (_m *Store) UpdateGithubCredentials(ctx context.Context, id uint, param par } // UpdateGithubEndpoint provides a mock function with given fields: ctx, name, param -func (_m *Store) UpdateGithubEndpoint(ctx context.Context, name string, param params.UpdateGithubEndpointParams) (params.GithubEndpoint, error) { +func (_m *Store) UpdateGithubEndpoint(ctx context.Context, name string, param params.UpdateGithubEndpointParams) (params.ForgeEndpoint, error) { ret := _m.Called(ctx, name, param) if len(ret) == 0 { panic("no return value specified for UpdateGithubEndpoint") } - var r0 params.GithubEndpoint + var r0 params.ForgeEndpoint var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, params.UpdateGithubEndpointParams) (params.GithubEndpoint, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, params.UpdateGithubEndpointParams) (params.ForgeEndpoint, error)); ok { return rf(ctx, name, param) } - if rf, ok := ret.Get(0).(func(context.Context, string, params.UpdateGithubEndpointParams) params.GithubEndpoint); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, params.UpdateGithubEndpointParams) params.ForgeEndpoint); ok { r0 = rf(ctx, name, param) } else { - r0 = ret.Get(0).(params.GithubEndpoint) + r0 = ret.Get(0).(params.ForgeEndpoint) } if rf, ok := ret.Get(1).(func(context.Context, string, params.UpdateGithubEndpointParams) error); ok { diff --git a/database/common/store.go b/database/common/store.go index 65fd13435..db5fbb048 100644 --- a/database/common/store.go +++ b/database/common/store.go @@ -21,24 +21,24 @@ import ( ) type GithubEndpointStore interface { - CreateGithubEndpoint(ctx context.Context, param params.CreateGithubEndpointParams) (params.GithubEndpoint, error) - GetGithubEndpoint(ctx context.Context, name string) (params.GithubEndpoint, error) - ListGithubEndpoints(ctx context.Context) ([]params.GithubEndpoint, error) - UpdateGithubEndpoint(ctx context.Context, name string, param params.UpdateGithubEndpointParams) (params.GithubEndpoint, error) + CreateGithubEndpoint(ctx context.Context, param params.CreateGithubEndpointParams) (params.ForgeEndpoint, error) + GetGithubEndpoint(ctx context.Context, name string) (params.ForgeEndpoint, error) + ListGithubEndpoints(ctx context.Context) ([]params.ForgeEndpoint, error) + UpdateGithubEndpoint(ctx context.Context, name string, param params.UpdateGithubEndpointParams) (params.ForgeEndpoint, error) DeleteGithubEndpoint(ctx context.Context, name string) error } type GithubCredentialsStore interface { - CreateGithubCredentials(ctx context.Context, param params.CreateGithubCredentialsParams) (params.GithubCredentials, error) - GetGithubCredentials(ctx context.Context, id uint, detailed bool) (params.GithubCredentials, error) - GetGithubCredentialsByName(ctx context.Context, name string, detailed bool) (params.GithubCredentials, error) - ListGithubCredentials(ctx context.Context) ([]params.GithubCredentials, error) - UpdateGithubCredentials(ctx context.Context, id uint, param params.UpdateGithubCredentialsParams) (params.GithubCredentials, error) + CreateGithubCredentials(ctx context.Context, param params.CreateGithubCredentialsParams) (params.ForgeCredentials, error) + GetGithubCredentials(ctx context.Context, id uint, detailed bool) (params.ForgeCredentials, error) + GetGithubCredentialsByName(ctx context.Context, name string, detailed bool) (params.ForgeCredentials, error) + ListGithubCredentials(ctx context.Context) ([]params.ForgeCredentials, error) + UpdateGithubCredentials(ctx context.Context, id uint, param params.UpdateGithubCredentialsParams) (params.ForgeCredentials, error) DeleteGithubCredentials(ctx context.Context, id uint) error } type RepoStore interface { - CreateRepository(ctx context.Context, owner, name, credentialsName, webhookSecret string, poolBalancerType params.PoolBalancerType) (params.Repository, error) + CreateRepository(ctx context.Context, owner, name string, credentials params.ForgeCredentials, webhookSecret string, poolBalancerType params.PoolBalancerType) (param params.Repository, err error) GetRepository(ctx context.Context, owner, name, endpointName string) (params.Repository, error) GetRepositoryByID(ctx context.Context, repoID string) (params.Repository, error) ListRepositories(ctx context.Context) ([]params.Repository, error) @@ -47,7 +47,7 @@ type RepoStore interface { } type OrgStore interface { - CreateOrganization(ctx context.Context, name, credentialsName, webhookSecret string, poolBalancerType params.PoolBalancerType) (params.Organization, error) + CreateOrganization(ctx context.Context, name string, credentials params.ForgeCredentials, webhookSecret string, poolBalancerType params.PoolBalancerType) (org params.Organization, err error) GetOrganization(ctx context.Context, name, endpointName string) (params.Organization, error) GetOrganizationByID(ctx context.Context, orgID string) (params.Organization, error) ListOrganizations(ctx context.Context) ([]params.Organization, error) @@ -56,7 +56,7 @@ type OrgStore interface { } type EnterpriseStore interface { - CreateEnterprise(ctx context.Context, name, credentialsName, webhookSecret string, poolBalancerType params.PoolBalancerType) (params.Enterprise, error) + CreateEnterprise(ctx context.Context, name string, credentialsName params.ForgeCredentials, webhookSecret string, poolBalancerType params.PoolBalancerType) (params.Enterprise, error) GetEnterprise(ctx context.Context, name, endpointName string) (params.Enterprise, error) GetEnterpriseByID(ctx context.Context, enterpriseID string) (params.Enterprise, error) ListEnterprises(ctx context.Context) ([]params.Enterprise, error) @@ -76,7 +76,7 @@ type PoolStore interface { PoolInstanceCount(ctx context.Context, poolID string) (int64, error) GetPoolInstanceByName(ctx context.Context, poolID string, instanceName string) (params.Instance, error) - FindPoolsMatchingAllTags(ctx context.Context, entityType params.GithubEntityType, entityID string, tags []string) ([]params.Pool, error) + FindPoolsMatchingAllTags(ctx context.Context, entityType params.ForgeEntityType, entityID string, tags []string) ([]params.Pool, error) } type UserStore interface { @@ -107,7 +107,7 @@ type InstanceStore interface { type JobsStore interface { CreateOrUpdateJob(ctx context.Context, job params.Job) (params.Job, error) - ListEntityJobsByStatus(ctx context.Context, entityType params.GithubEntityType, entityID string, status params.JobStatus) ([]params.Job, error) + ListEntityJobsByStatus(ctx context.Context, entityType params.ForgeEntityType, entityID string, status params.JobStatus) ([]params.Job, error) ListJobsByStatus(ctx context.Context, status params.JobStatus) ([]params.Job, error) ListAllJobs(ctx context.Context) ([]params.Job, error) @@ -121,13 +121,13 @@ type JobsStore interface { } type EntityPoolStore interface { - CreateEntityPool(ctx context.Context, entity params.GithubEntity, param params.CreatePoolParams) (params.Pool, error) - GetEntityPool(ctx context.Context, entity params.GithubEntity, poolID string) (params.Pool, error) - DeleteEntityPool(ctx context.Context, entity params.GithubEntity, poolID string) error - UpdateEntityPool(ctx context.Context, entity params.GithubEntity, poolID string, param params.UpdatePoolParams) (params.Pool, error) + CreateEntityPool(ctx context.Context, entity params.ForgeEntity, param params.CreatePoolParams) (params.Pool, error) + GetEntityPool(ctx context.Context, entity params.ForgeEntity, poolID string) (params.Pool, error) + DeleteEntityPool(ctx context.Context, entity params.ForgeEntity, poolID string) error + UpdateEntityPool(ctx context.Context, entity params.ForgeEntity, poolID string, param params.UpdatePoolParams) (params.Pool, error) - ListEntityPools(ctx context.Context, entity params.GithubEntity) ([]params.Pool, error) - ListEntityInstances(ctx context.Context, entity params.GithubEntity) ([]params.Instance, error) + ListEntityPools(ctx context.Context, entity params.ForgeEntity) ([]params.Pool, error) + ListEntityInstances(ctx context.Context, entity params.ForgeEntity) ([]params.Instance, error) } type ControllerStore interface { @@ -138,9 +138,9 @@ type ControllerStore interface { type ScaleSetsStore interface { ListAllScaleSets(ctx context.Context) ([]params.ScaleSet, error) - CreateEntityScaleSet(_ context.Context, entity params.GithubEntity, param params.CreateScaleSetParams) (scaleSet params.ScaleSet, err error) - ListEntityScaleSets(_ context.Context, entity params.GithubEntity) ([]params.ScaleSet, error) - UpdateEntityScaleSet(_ context.Context, entity params.GithubEntity, scaleSetID uint, param params.UpdateScaleSetParams, callback func(old, newSet params.ScaleSet) error) (updatedScaleSet params.ScaleSet, err error) + CreateEntityScaleSet(_ context.Context, entity params.ForgeEntity, param params.CreateScaleSetParams) (scaleSet params.ScaleSet, err error) + ListEntityScaleSets(_ context.Context, entity params.ForgeEntity) ([]params.ScaleSet, error) + UpdateEntityScaleSet(_ context.Context, entity params.ForgeEntity, scaleSetID uint, param params.UpdateScaleSetParams, callback func(old, newSet params.ScaleSet) error) (updatedScaleSet params.ScaleSet, err error) GetScaleSetByID(ctx context.Context, scaleSet uint) (params.ScaleSet, error) DeleteScaleSetByID(ctx context.Context, scaleSetID uint) (err error) SetScaleSetLastMessageID(ctx context.Context, scaleSetID uint, lastMessageID int64) error @@ -152,6 +152,23 @@ type ScaleSetInstanceStore interface { CreateScaleSetInstance(_ context.Context, scaleSetID uint, param params.CreateInstanceParams) (instance params.Instance, err error) } +type GiteaEndpointStore interface { + CreateGiteaEndpoint(_ context.Context, param params.CreateGiteaEndpointParams) (ghEndpoint params.ForgeEndpoint, err error) + ListGiteaEndpoints(_ context.Context) ([]params.ForgeEndpoint, error) + DeleteGiteaEndpoint(_ context.Context, name string) (err error) + GetGiteaEndpoint(_ context.Context, name string) (params.ForgeEndpoint, error) + UpdateGiteaEndpoint(_ context.Context, name string, param params.UpdateGiteaEndpointParams) (ghEndpoint params.ForgeEndpoint, err error) +} + +type GiteaCredentialsStore interface { + CreateGiteaCredentials(ctx context.Context, param params.CreateGiteaCredentialsParams) (gtCreds params.ForgeCredentials, err error) + GetGiteaCredentialsByName(ctx context.Context, name string, detailed bool) (params.ForgeCredentials, error) + GetGiteaCredentials(ctx context.Context, id uint, detailed bool) (params.ForgeCredentials, error) + ListGiteaCredentials(ctx context.Context) ([]params.ForgeCredentials, error) + UpdateGiteaCredentials(ctx context.Context, id uint, param params.UpdateGiteaCredentialsParams) (gtCreds params.ForgeCredentials, err error) + DeleteGiteaCredentials(ctx context.Context, id uint) (err error) +} + //go:generate mockery --name=Store type Store interface { RepoStore @@ -167,9 +184,11 @@ type Store interface { EntityPoolStore ScaleSetsStore ScaleSetInstanceStore + GiteaEndpointStore + GiteaCredentialsStore ControllerInfo() (params.ControllerInfo, error) InitController() (params.ControllerInfo, error) - GetGithubEntity(_ context.Context, entityType params.GithubEntityType, entityID string) (params.GithubEntity, error) - AddEntityEvent(ctx context.Context, entity params.GithubEntity, event params.EventType, eventLevel params.EventLevel, statusMessage string, maxEvents int) error + GetForgeEntity(_ context.Context, entityType params.ForgeEntityType, entityID string) (params.ForgeEntity, error) + AddEntityEvent(ctx context.Context, entity params.ForgeEntity, event params.EventType, eventLevel params.EventLevel, statusMessage string, maxEvents int) error } diff --git a/database/common/watcher.go b/database/common/watcher.go index 85df11512..94152094a 100644 --- a/database/common/watcher.go +++ b/database/common/watcher.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package common import "context" @@ -18,6 +32,7 @@ const ( JobEntityType DatabaseEntityType = "job" ControllerEntityType DatabaseEntityType = "controller" GithubCredentialsEntityType DatabaseEntityType = "github_credentials" // #nosec G101 + GiteaCredentialsEntityType DatabaseEntityType = "gitea_credentials" // #nosec G101 GithubEndpointEntityType DatabaseEntityType = "github_endpoint" ScaleSetEntityType DatabaseEntityType = "scaleset" ) diff --git a/database/sql/common_test.go b/database/sql/common_test.go index af0adcf90..a3c62e062 100644 --- a/database/sql/common_test.go +++ b/database/sql/common_test.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package sql const ( diff --git a/database/sql/enterprise.go b/database/sql/enterprise.go index 414a7aafa..e9c2ed087 100644 --- a/database/sql/enterprise.go +++ b/database/sql/enterprise.go @@ -28,10 +28,14 @@ import ( "github.com/cloudbase/garm/params" ) -func (s *sqlDatabase) CreateEnterprise(ctx context.Context, name, credentialsName, webhookSecret string, poolBalancerType params.PoolBalancerType) (paramEnt params.Enterprise, err error) { +func (s *sqlDatabase) CreateEnterprise(ctx context.Context, name string, credentials params.ForgeCredentials, webhookSecret string, poolBalancerType params.PoolBalancerType) (paramEnt params.Enterprise, err error) { if webhookSecret == "" { return params.Enterprise{}, errors.New("creating enterprise: missing secret") } + if credentials.ForgeType != params.GithubEndpointType { + return params.Enterprise{}, errors.Wrap(runnerErrors.ErrBadRequest, "enterprises are not supported on this forge type") + } + secret, err := util.Seal([]byte(webhookSecret), []byte(s.cfg.Passphrase)) if err != nil { return params.Enterprise{}, errors.Wrap(err, "encoding secret") @@ -45,29 +49,21 @@ func (s *sqlDatabase) CreateEnterprise(ctx context.Context, name, credentialsNam newEnterprise := Enterprise{ Name: name, WebhookSecret: secret, - CredentialsName: credentialsName, PoolBalancerType: poolBalancerType, } err = s.conn.Transaction(func(tx *gorm.DB) error { - creds, err := s.getGithubCredentialsByName(ctx, tx, credentialsName, false) - if err != nil { - return errors.Wrap(err, "creating enterprise") - } - if creds.EndpointName == nil { - return errors.Wrap(runnerErrors.ErrUnprocessable, "credentials have no endpoint") - } - newEnterprise.CredentialsID = &creds.ID - newEnterprise.CredentialsName = creds.Name - newEnterprise.EndpointName = creds.EndpointName + newEnterprise.CredentialsID = &credentials.ID + newEnterprise.EndpointName = &credentials.Endpoint.Name q := tx.Create(&newEnterprise) if q.Error != nil { return errors.Wrap(q.Error, "creating enterprise") } - newEnterprise.Credentials = creds - newEnterprise.Endpoint = creds.Endpoint - + newEnterprise, err = s.getEnterpriseByID(ctx, tx, newEnterprise.ID.String(), "Pools", "Credentials", "Endpoint", "Credentials.Endpoint") + if err != nil { + return errors.Wrap(err, "creating enterprise") + } return nil }) if err != nil { diff --git a/database/sql/enterprise_test.go b/database/sql/enterprise_test.go index 3cfcbc324..24e7bee7a 100644 --- a/database/sql/enterprise_test.go +++ b/database/sql/enterprise_test.go @@ -53,9 +53,9 @@ type EnterpriseTestSuite struct { adminCtx context.Context adminUserID string - testCreds params.GithubCredentials - secondaryTestCreds params.GithubCredentials - githubEndpoint params.GithubEndpoint + testCreds params.ForgeCredentials + secondaryTestCreds params.ForgeCredentials + githubEndpoint params.ForgeEndpoint } func (s *EnterpriseTestSuite) equalInstancesByName(expected, actual []params.Instance) { @@ -99,7 +99,7 @@ func (s *EnterpriseTestSuite) SetupTest() { enterprise, err := db.CreateEnterprise( s.adminCtx, fmt.Sprintf("test-enterprise-%d", i), - s.testCreds.Name, + s.testCreds, fmt.Sprintf("test-webhook-secret-%d", i), params.PoolBalancerTypeRoundRobin, ) @@ -178,7 +178,7 @@ func (s *EnterpriseTestSuite) TestCreateEnterprise() { enterprise, err := s.Store.CreateEnterprise( s.adminCtx, s.Fixtures.CreateEnterpriseParams.Name, - s.Fixtures.CreateEnterpriseParams.CredentialsName, + s.testCreds, s.Fixtures.CreateEnterpriseParams.WebhookSecret, params.PoolBalancerTypeRoundRobin) @@ -209,7 +209,7 @@ func (s *EnterpriseTestSuite) TestCreateEnterpriseInvalidDBPassphrase() { _, err = sqlDB.CreateEnterprise( s.adminCtx, s.Fixtures.CreateEnterpriseParams.Name, - s.Fixtures.CreateEnterpriseParams.CredentialsName, + s.testCreds, s.Fixtures.CreateEnterpriseParams.WebhookSecret, params.PoolBalancerTypeRoundRobin) @@ -219,14 +219,6 @@ func (s *EnterpriseTestSuite) TestCreateEnterpriseInvalidDBPassphrase() { func (s *EnterpriseTestSuite) TestCreateEnterpriseDBCreateErr() { s.Fixtures.SQLMock.ExpectBegin() - s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE user_id = ? AND name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). - WithArgs(s.adminUserID, s.Fixtures.Enterprises[0].CredentialsName, 1). - WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}).AddRow(s.testCreds.ID, s.testCreds.Endpoint.Name)) - s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). - WithArgs(s.testCreds.Endpoint.Name). - WillReturnRows(sqlmock.NewRows([]string{"name"}). - AddRow(s.testCreds.Endpoint.Name)) s.Fixtures.SQLMock. ExpectExec(regexp.QuoteMeta("INSERT INTO `enterprises`")). WillReturnError(fmt.Errorf("creating enterprise mock error")) @@ -235,7 +227,7 @@ func (s *EnterpriseTestSuite) TestCreateEnterpriseDBCreateErr() { _, err := s.StoreSQLMocked.CreateEnterprise( s.adminCtx, s.Fixtures.CreateEnterpriseParams.Name, - s.Fixtures.CreateEnterpriseParams.CredentialsName, + s.testCreds, s.Fixtures.CreateEnterpriseParams.WebhookSecret, params.PoolBalancerTypeRoundRobin) @@ -490,9 +482,9 @@ func (s *EnterpriseTestSuite) TestCreateEnterprisePoolMissingTags() { } func (s *EnterpriseTestSuite) TestCreateEnterprisePoolInvalidEnterpriseID() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "dummy-enterprise-id", - EntityType: params.GithubEntityTypeEnterprise, + EntityType: params.ForgeEntityTypeEnterprise, } _, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) @@ -637,9 +629,9 @@ func (s *EnterpriseTestSuite) TestListEnterprisePools() { } func (s *EnterpriseTestSuite) TestListEnterprisePoolsInvalidEnterpriseID() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "dummy-enterprise-id", - EntityType: params.GithubEntityTypeEnterprise, + EntityType: params.ForgeEntityTypeEnterprise, } _, err := s.Store.ListEntityPools(s.adminCtx, entity) @@ -662,9 +654,9 @@ func (s *EnterpriseTestSuite) TestGetEnterprisePool() { } func (s *EnterpriseTestSuite) TestGetEnterprisePoolInvalidEnterpriseID() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "dummy-enterprise-id", - EntityType: params.GithubEntityTypeEnterprise, + EntityType: params.ForgeEntityTypeEnterprise, } _, err := s.Store.GetEntityPool(s.adminCtx, entity, "dummy-pool-id") @@ -688,9 +680,9 @@ func (s *EnterpriseTestSuite) TestDeleteEnterprisePool() { } func (s *EnterpriseTestSuite) TestDeleteEnterprisePoolInvalidEnterpriseID() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "dummy-enterprise-id", - EntityType: params.GithubEntityTypeEnterprise, + EntityType: params.ForgeEntityTypeEnterprise, } err := s.Store.DeleteEntityPool(s.adminCtx, entity, "dummy-pool-id") @@ -743,9 +735,9 @@ func (s *EnterpriseTestSuite) TestListEnterpriseInstances() { } func (s *EnterpriseTestSuite) TestListEnterpriseInstancesInvalidEnterpriseID() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "dummy-enterprise-id", - EntityType: params.GithubEntityTypeEnterprise, + EntityType: params.ForgeEntityTypeEnterprise, } _, err := s.Store.ListEntityInstances(s.adminCtx, entity) @@ -771,9 +763,9 @@ func (s *EnterpriseTestSuite) TestUpdateEnterprisePool() { } func (s *EnterpriseTestSuite) TestUpdateEnterprisePoolInvalidEnterpriseID() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "dummy-enterprise-id", - EntityType: params.GithubEntityTypeEnterprise, + EntityType: params.ForgeEntityTypeEnterprise, } _, err := s.Store.UpdateEntityPool(s.adminCtx, entity, "dummy-pool-id", s.Fixtures.UpdatePoolParams) diff --git a/database/sql/gitea.go b/database/sql/gitea.go new file mode 100644 index 000000000..3b4c55ecb --- /dev/null +++ b/database/sql/gitea.go @@ -0,0 +1,478 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package sql + +import ( + "context" + "log/slog" + + "github.com/pkg/errors" + "gorm.io/gorm" + + runnerErrors "github.com/cloudbase/garm-provider-common/errors" + "github.com/cloudbase/garm/auth" + "github.com/cloudbase/garm/database/common" + "github.com/cloudbase/garm/params" +) + +func (s *sqlDatabase) CreateGiteaEndpoint(_ context.Context, param params.CreateGiteaEndpointParams) (ghEndpoint params.ForgeEndpoint, err error) { + defer func() { + if err == nil { + s.sendNotify(common.GithubEndpointEntityType, common.CreateOperation, ghEndpoint) + } + }() + var endpoint GithubEndpoint + err = s.conn.Transaction(func(tx *gorm.DB) error { + if err := tx.Where("name = ?", param.Name).First(&endpoint).Error; err == nil { + return errors.Wrap(runnerErrors.ErrDuplicateEntity, "gitea endpoint already exists") + } + endpoint = GithubEndpoint{ + Name: param.Name, + Description: param.Description, + APIBaseURL: param.APIBaseURL, + BaseURL: param.BaseURL, + CACertBundle: param.CACertBundle, + EndpointType: params.GiteaEndpointType, + } + + if err := tx.Create(&endpoint).Error; err != nil { + return errors.Wrap(err, "creating gitea endpoint") + } + return nil + }) + if err != nil { + return params.ForgeEndpoint{}, errors.Wrap(err, "creating gitea endpoint") + } + ghEndpoint, err = s.sqlToCommonGithubEndpoint(endpoint) + if err != nil { + return params.ForgeEndpoint{}, errors.Wrap(err, "converting gitea endpoint") + } + return ghEndpoint, nil +} + +func (s *sqlDatabase) ListGiteaEndpoints(_ context.Context) ([]params.ForgeEndpoint, error) { + var endpoints []GithubEndpoint + err := s.conn.Where("endpoint_type = ?", params.GiteaEndpointType).Find(&endpoints).Error + if err != nil { + return nil, errors.Wrap(err, "fetching gitea endpoints") + } + + var ret []params.ForgeEndpoint + for _, ep := range endpoints { + commonEp, err := s.sqlToCommonGithubEndpoint(ep) + if err != nil { + return nil, errors.Wrap(err, "converting gitea endpoint") + } + ret = append(ret, commonEp) + } + return ret, nil +} + +func (s *sqlDatabase) UpdateGiteaEndpoint(_ context.Context, name string, param params.UpdateGiteaEndpointParams) (ghEndpoint params.ForgeEndpoint, err error) { + defer func() { + if err == nil { + s.sendNotify(common.GithubEndpointEntityType, common.UpdateOperation, ghEndpoint) + } + }() + var endpoint GithubEndpoint + err = s.conn.Transaction(func(tx *gorm.DB) error { + if err := tx.Where("name = ? and endpoint_type = ?", name, params.GiteaEndpointType).First(&endpoint).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrap(runnerErrors.ErrNotFound, "gitea endpoint not found") + } + return errors.Wrap(err, "fetching gitea endpoint") + } + if param.APIBaseURL != nil { + endpoint.APIBaseURL = *param.APIBaseURL + } + + if param.BaseURL != nil { + endpoint.BaseURL = *param.BaseURL + } + + if param.CACertBundle != nil { + endpoint.CACertBundle = param.CACertBundle + } + + if param.Description != nil { + endpoint.Description = *param.Description + } + + if err := tx.Save(&endpoint).Error; err != nil { + return errors.Wrap(err, "updating gitea endpoint") + } + + return nil + }) + if err != nil { + return params.ForgeEndpoint{}, errors.Wrap(err, "updating gitea endpoint") + } + ghEndpoint, err = s.sqlToCommonGithubEndpoint(endpoint) + if err != nil { + return params.ForgeEndpoint{}, errors.Wrap(err, "converting gitea endpoint") + } + return ghEndpoint, nil +} + +func (s *sqlDatabase) GetGiteaEndpoint(_ context.Context, name string) (params.ForgeEndpoint, error) { + var endpoint GithubEndpoint + err := s.conn.Where("name = ? and endpoint_type = ?", name, params.GiteaEndpointType).First(&endpoint).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return params.ForgeEndpoint{}, errors.Wrap(runnerErrors.ErrNotFound, "gitea endpoint not found") + } + return params.ForgeEndpoint{}, errors.Wrap(err, "fetching gitea endpoint") + } + + return s.sqlToCommonGithubEndpoint(endpoint) +} + +func (s *sqlDatabase) DeleteGiteaEndpoint(_ context.Context, name string) (err error) { + if name == defaultGithubEndpoint { + return runnerErrors.NewBadRequestError("cannot delete default endpoint %s", defaultGithubEndpoint) + } + + defer func() { + if err == nil { + s.sendNotify(common.GithubEndpointEntityType, common.DeleteOperation, params.ForgeEndpoint{Name: name}) + } + }() + err = s.conn.Transaction(func(tx *gorm.DB) error { + var endpoint GithubEndpoint + if err := tx.Where("name = ? and endpoint_type = ?", name, params.GiteaEndpointType).First(&endpoint).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return errors.Wrap(err, "fetching gitea endpoint") + } + + var credsCount int64 + if err := tx.Model(&GiteaCredentials{}).Where("endpoint_name = ?", endpoint.Name).Count(&credsCount).Error; err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrap(err, "fetching gitea credentials") + } + } + + var repoCnt int64 + if err := tx.Model(&Repository{}).Where("endpoint_name = ?", endpoint.Name).Count(&repoCnt).Error; err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrap(err, "fetching gitea repositories") + } + } + + var orgCnt int64 + if err := tx.Model(&Organization{}).Where("endpoint_name = ?", endpoint.Name).Count(&orgCnt).Error; err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrap(err, "fetching gitea organizations") + } + } + + if credsCount > 0 || repoCnt > 0 || orgCnt > 0 { + return runnerErrors.NewBadRequestError("cannot delete endpoint with associated entities") + } + + if err := tx.Unscoped().Delete(&endpoint).Error; err != nil { + return errors.Wrap(err, "deleting gitea endpoint") + } + return nil + }) + if err != nil { + return errors.Wrap(err, "deleting gitea endpoint") + } + return nil +} + +func (s *sqlDatabase) CreateGiteaCredentials(ctx context.Context, param params.CreateGiteaCredentialsParams) (gtCreds params.ForgeCredentials, err error) { + userID, err := getUIDFromContext(ctx) + if err != nil { + return params.ForgeCredentials{}, errors.Wrap(err, "creating gitea credentials") + } + if param.Endpoint == "" { + return params.ForgeCredentials{}, errors.Wrap(runnerErrors.ErrBadRequest, "endpoint name is required") + } + + defer func() { + if err == nil { + s.sendNotify(common.GiteaCredentialsEntityType, common.CreateOperation, gtCreds) + } + }() + var creds GiteaCredentials + err = s.conn.Transaction(func(tx *gorm.DB) error { + var endpoint GithubEndpoint + if err := tx.Where("name = ? and endpoint_type = ?", param.Endpoint, params.GiteaEndpointType).First(&endpoint).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrap(runnerErrors.ErrNotFound, "gitea endpoint not found") + } + return errors.Wrap(err, "fetching gitea endpoint") + } + + if err := tx.Where("name = ? and user_id = ?", param.Name, userID).First(&creds).Error; err == nil { + return errors.Wrap(runnerErrors.ErrDuplicateEntity, "gitea credentials already exists") + } + + var data []byte + var err error + switch param.AuthType { + case params.ForgeAuthTypePAT: + data, err = s.marshalAndSeal(param.PAT) + default: + return errors.Wrap(runnerErrors.ErrBadRequest, "invalid auth type") + } + if err != nil { + return errors.Wrap(err, "marshaling and sealing credentials") + } + + creds = GiteaCredentials{ + Name: param.Name, + Description: param.Description, + EndpointName: &endpoint.Name, + AuthType: param.AuthType, + Payload: data, + UserID: &userID, + } + + if err := tx.Create(&creds).Error; err != nil { + return errors.Wrap(err, "creating gitea credentials") + } + // Skip making an extra query. + creds.Endpoint = endpoint + + return nil + }) + if err != nil { + return params.ForgeCredentials{}, errors.Wrap(err, "creating gitea credentials") + } + gtCreds, err = s.sqlGiteaToCommonForgeCredentials(creds) + if err != nil { + return params.ForgeCredentials{}, errors.Wrap(err, "converting gitea credentials") + } + return gtCreds, nil +} + +func (s *sqlDatabase) getGiteaCredentialsByName(ctx context.Context, tx *gorm.DB, name string, detailed bool) (GiteaCredentials, error) { + var creds GiteaCredentials + q := tx.Preload("Endpoint") + + if detailed { + q = q. + Preload("Repositories"). + Preload("Organizations"). + Preload("Repositories.GiteaCredentials"). + Preload("Organizations.GiteaCredentials"). + Preload("Repositories.Credentials"). + Preload("Organizations.Credentials") + } + + userID, err := getUIDFromContext(ctx) + if err != nil { + return GiteaCredentials{}, errors.Wrap(err, "fetching gitea credentials") + } + q = q.Where("user_id = ?", userID) + + err = q.Where("name = ?", name).First(&creds).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return GiteaCredentials{}, errors.Wrap(runnerErrors.ErrNotFound, "gitea credentials not found") + } + return GiteaCredentials{}, errors.Wrap(err, "fetching gitea credentials") + } + + return creds, nil +} + +func (s *sqlDatabase) GetGiteaCredentialsByName(ctx context.Context, name string, detailed bool) (params.ForgeCredentials, error) { + creds, err := s.getGiteaCredentialsByName(ctx, s.conn, name, detailed) + if err != nil { + return params.ForgeCredentials{}, errors.Wrap(err, "fetching gitea credentials") + } + + return s.sqlGiteaToCommonForgeCredentials(creds) +} + +func (s *sqlDatabase) GetGiteaCredentials(ctx context.Context, id uint, detailed bool) (params.ForgeCredentials, error) { + var creds GiteaCredentials + q := s.conn.Preload("Endpoint") + + if detailed { + q = q. + Preload("Repositories"). + Preload("Organizations"). + Preload("Repositories.GiteaCredentials"). + Preload("Organizations.GiteaCredentials"). + Preload("Repositories.Credentials"). + Preload("Organizations.Credentials") + } + + if !auth.IsAdmin(ctx) { + userID, err := getUIDFromContext(ctx) + if err != nil { + return params.ForgeCredentials{}, errors.Wrap(err, "fetching gitea credentials") + } + q = q.Where("user_id = ?", userID) + } + + err := q.Where("id = ?", id).First(&creds).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return params.ForgeCredentials{}, errors.Wrap(runnerErrors.ErrNotFound, "gitea credentials not found") + } + return params.ForgeCredentials{}, errors.Wrap(err, "fetching gitea credentials") + } + + return s.sqlGiteaToCommonForgeCredentials(creds) +} + +func (s *sqlDatabase) ListGiteaCredentials(ctx context.Context) ([]params.ForgeCredentials, error) { + q := s.conn.Preload("Endpoint") + if !auth.IsAdmin(ctx) { + userID, err := getUIDFromContext(ctx) + if err != nil { + return nil, errors.Wrap(err, "fetching gitea credentials") + } + q = q.Where("user_id = ?", userID) + } + + var creds []GiteaCredentials + err := q.Preload("Endpoint").Find(&creds).Error + if err != nil { + return nil, errors.Wrap(err, "fetching gitea credentials") + } + + var ret []params.ForgeCredentials + for _, c := range creds { + commonCreds, err := s.sqlGiteaToCommonForgeCredentials(c) + if err != nil { + return nil, errors.Wrap(err, "converting gitea credentials") + } + ret = append(ret, commonCreds) + } + return ret, nil +} + +func (s *sqlDatabase) UpdateGiteaCredentials(ctx context.Context, id uint, param params.UpdateGiteaCredentialsParams) (gtCreds params.ForgeCredentials, err error) { + defer func() { + if err == nil { + s.sendNotify(common.GiteaCredentialsEntityType, common.UpdateOperation, gtCreds) + } + }() + var creds GiteaCredentials + err = s.conn.Transaction(func(tx *gorm.DB) error { + q := tx.Preload("Endpoint") + if !auth.IsAdmin(ctx) { + userID, err := getUIDFromContext(ctx) + if err != nil { + return errors.Wrap(err, "updating gitea credentials") + } + q = q.Where("user_id = ?", userID) + } + + if err := q.Where("id = ?", id).First(&creds).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrap(runnerErrors.ErrNotFound, "gitea credentials not found") + } + return errors.Wrap(err, "fetching gitea credentials") + } + + if param.Name != nil { + creds.Name = *param.Name + } + if param.Description != nil { + creds.Description = *param.Description + } + + var data []byte + var err error + switch creds.AuthType { + case params.ForgeAuthTypePAT: + if param.PAT != nil { + data, err = s.marshalAndSeal(param.PAT) + } + default: + return errors.Wrap(runnerErrors.ErrBadRequest, "invalid auth type") + } + + if err != nil { + return errors.Wrap(err, "marshaling and sealing credentials") + } + if len(data) > 0 { + creds.Payload = data + } + + if err := tx.Save(&creds).Error; err != nil { + return errors.Wrap(err, "updating gitea credentials") + } + return nil + }) + if err != nil { + return params.ForgeCredentials{}, errors.Wrap(err, "updating gitea credentials") + } + + gtCreds, err = s.sqlGiteaToCommonForgeCredentials(creds) + if err != nil { + return params.ForgeCredentials{}, errors.Wrap(err, "converting gitea credentials") + } + return gtCreds, nil +} + +func (s *sqlDatabase) DeleteGiteaCredentials(ctx context.Context, id uint) (err error) { + var creds GiteaCredentials + defer func() { + if err == nil { + forgeCreds, innerErr := s.sqlGiteaToCommonForgeCredentials(creds) + if innerErr != nil { + slog.ErrorContext(ctx, "converting gitea credentials", "error", innerErr) + } + if creds.ID == 0 || creds.Name == "" { + return + } + s.sendNotify(common.GiteaCredentialsEntityType, common.DeleteOperation, forgeCreds) + } + }() + err = s.conn.Transaction(func(tx *gorm.DB) error { + q := tx.Where("id = ?", id). + Preload("Repositories"). + Preload("Organizations") + if !auth.IsAdmin(ctx) { + userID, err := getUIDFromContext(ctx) + if err != nil { + return errors.Wrap(err, "deleting gitea credentials") + } + q = q.Where("user_id = ?", userID) + } + + err := q.First(&creds).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return errors.Wrap(err, "fetching gitea credentials") + } + + if len(creds.Repositories) > 0 { + return errors.Wrap(runnerErrors.ErrBadRequest, "cannot delete credentials with repositories") + } + if len(creds.Organizations) > 0 { + return errors.Wrap(runnerErrors.ErrBadRequest, "cannot delete credentials with organizations") + } + if err := tx.Unscoped().Delete(&creds).Error; err != nil { + return errors.Wrap(err, "deleting gitea credentials") + } + return nil + }) + if err != nil { + return errors.Wrap(err, "deleting gitea credentials") + } + return nil +} diff --git a/database/sql/gitea_test.go b/database/sql/gitea_test.go new file mode 100644 index 000000000..a70d3b1f3 --- /dev/null +++ b/database/sql/gitea_test.go @@ -0,0 +1,793 @@ +// Copyright 2024 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package sql + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/suite" + + runnerErrors "github.com/cloudbase/garm-provider-common/errors" + "github.com/cloudbase/garm/auth" + "github.com/cloudbase/garm/database/common" + garmTesting "github.com/cloudbase/garm/internal/testing" + "github.com/cloudbase/garm/params" +) + +type GiteaTestSuite struct { + suite.Suite + + giteaEndpoint params.ForgeEndpoint + db common.Store +} + +func (s *GiteaTestSuite) SetupTest() { + db, err := NewSQLDatabase(context.Background(), garmTesting.GetTestSqliteDBConfig(s.T())) + if err != nil { + s.FailNow(fmt.Sprintf("failed to create db connection: %s", err)) + } + + s.db = db + + createEpParams := params.CreateGiteaEndpointParams{ + Name: testEndpointName, + Description: testEndpointDescription, + APIBaseURL: testAPIBaseURL, + BaseURL: testBaseURL, + } + endpoint, err := s.db.CreateGiteaEndpoint(context.Background(), createEpParams) + s.Require().NoError(err) + s.Require().NotNil(endpoint) + s.Require().Equal(testEndpointName, endpoint.Name) + s.giteaEndpoint = endpoint +} + +func (s *GiteaTestSuite) TestCreatingEndpoint() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + createEpParams := params.CreateGiteaEndpointParams{ + Name: alternetTestEndpointName, + Description: testEndpointDescription, + APIBaseURL: testAPIBaseURL, + BaseURL: testBaseURL, + } + + endpoint, err := s.db.CreateGiteaEndpoint(ctx, createEpParams) + s.Require().NoError(err) + s.Require().NotNil(endpoint) + s.Require().Equal(alternetTestEndpointName, endpoint.Name) +} + +func (s *GiteaTestSuite) TestCreatingDuplicateEndpointFails() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + createEpParams := params.CreateGiteaEndpointParams{ + Name: alternetTestEndpointName, + Description: testEndpointDescription, + APIBaseURL: testAPIBaseURL, + BaseURL: testBaseURL, + } + + _, err := s.db.CreateGiteaEndpoint(ctx, createEpParams) + s.Require().NoError(err) + + _, err = s.db.CreateGiteaEndpoint(ctx, createEpParams) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrDuplicateEntity) +} + +func (s *GiteaTestSuite) TestGetEndpoint() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + createEpParams := params.CreateGiteaEndpointParams{ + Name: alternetTestEndpointName, + Description: testEndpointDescription, + APIBaseURL: testAPIBaseURL, + BaseURL: testBaseURL, + } + + newEndpoint, err := s.db.CreateGiteaEndpoint(ctx, createEpParams) + s.Require().NoError(err) + + endpoint, err := s.db.GetGiteaEndpoint(ctx, createEpParams.Name) + s.Require().NoError(err) + s.Require().NotNil(endpoint) + s.Require().Equal(newEndpoint.Name, endpoint.Name) +} + +func (s *GiteaTestSuite) TestGetNonExistingEndpointFailsWithNotFoundError() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + _, err := s.db.GetGiteaEndpoint(ctx, "non-existing") + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GiteaTestSuite) TestDeletingNonExistingEndpointIsANoop() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + err := s.db.DeleteGiteaEndpoint(ctx, "non-existing") + s.Require().NoError(err) +} + +func (s *GiteaTestSuite) TestDeletingEndpoint() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + createEpParams := params.CreateGiteaEndpointParams{ + Name: alternetTestEndpointName, + Description: testEndpointDescription, + APIBaseURL: testAPIBaseURL, + BaseURL: testBaseURL, + } + + endpoint, err := s.db.CreateGiteaEndpoint(ctx, createEpParams) + s.Require().NoError(err) + s.Require().NotNil(endpoint) + + err = s.db.DeleteGiteaEndpoint(ctx, alternetTestEndpointName) + s.Require().NoError(err) + + _, err = s.db.GetGiteaEndpoint(ctx, alternetTestEndpointName) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GiteaTestSuite) TestUpdateEndpoint() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + createEpParams := params.CreateGiteaEndpointParams{ + Name: "deleteme", + Description: testEndpointDescription, + APIBaseURL: testAPIBaseURL, + BaseURL: testBaseURL, + } + + endpoint, err := s.db.CreateGiteaEndpoint(ctx, createEpParams) + s.Require().NoError(err) + s.Require().NotNil(endpoint) + + newDescription := "another description" + newAPIBaseURL := "https://new-api.example.com" + newBaseURL := "https://new.example.com" + caCertBundle, err := os.ReadFile("../../testdata/certs/srv-pub.pem") + s.Require().NoError(err) + updateEpParams := params.UpdateGiteaEndpointParams{ + Description: &newDescription, + APIBaseURL: &newAPIBaseURL, + BaseURL: &newBaseURL, + CACertBundle: caCertBundle, + } + + updatedEndpoint, err := s.db.UpdateGiteaEndpoint(ctx, testEndpointName, updateEpParams) + s.Require().NoError(err) + s.Require().NotNil(updatedEndpoint) + s.Require().Equal(newDescription, updatedEndpoint.Description) + s.Require().Equal(newAPIBaseURL, updatedEndpoint.APIBaseURL) + s.Require().Equal(newBaseURL, updatedEndpoint.BaseURL) + s.Require().Equal(caCertBundle, updatedEndpoint.CACertBundle) +} + +func (s *GiteaTestSuite) TestUpdatingNonExistingEndpointReturnsNotFoundError() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + newDescription := "test desc" + updateEpParams := params.UpdateGiteaEndpointParams{ + Description: &newDescription, + } + + _, err := s.db.UpdateGiteaEndpoint(ctx, "non-existing", updateEpParams) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GiteaTestSuite) TestListEndpoints() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + createEpParams := params.CreateGiteaEndpointParams{ + Name: alternetTestEndpointName, + Description: testEndpointDescription, + APIBaseURL: testAPIBaseURL, + BaseURL: testBaseURL, + } + + _, err := s.db.CreateGiteaEndpoint(ctx, createEpParams) + s.Require().NoError(err) + + endpoints, err := s.db.ListGiteaEndpoints(ctx) + s.Require().NoError(err) + s.Require().Len(endpoints, 2) +} + +func (s *GiteaTestSuite) TestCreateCredentialsFailsWithUnauthorizedForAnonUser() { + ctx := context.Background() + + _, err := s.db.CreateGiteaCredentials(ctx, params.CreateGiteaCredentialsParams{}) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized) +} + +func (s *GiteaTestSuite) TestCreateCredentialsFailsWhenEndpointNameIsEmpty() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + _, err := s.db.CreateGiteaCredentials(ctx, params.CreateGiteaCredentialsParams{}) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrBadRequest) + s.Require().Regexp("endpoint name is required", err.Error()) +} + +func (s *GiteaTestSuite) TestCreateCredentialsFailsWhenEndpointDoesNotExist() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + _, err := s.db.CreateGiteaCredentials(ctx, params.CreateGiteaCredentialsParams{Endpoint: "non-existing"}) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) + s.Require().Regexp("endpoint not found", err.Error()) +} + +func (s *GiteaTestSuite) TestCreateCredentialsFailsWhenAuthTypeIsInvalid() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + _, err := s.db.CreateGiteaCredentials(ctx, params.CreateGiteaCredentialsParams{Endpoint: s.giteaEndpoint.Name, AuthType: "invalid"}) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrBadRequest) + s.Require().Regexp("invalid auth type", err.Error()) +} + +func (s *GiteaTestSuite) TestCreateCredentials() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + credParams := params.CreateGiteaCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: s.giteaEndpoint.Name, + AuthType: params.ForgeAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test", + }, + } + + creds, err := s.db.CreateGiteaCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + s.Require().Equal(credParams.Name, creds.Name) + s.Require().Equal(credParams.Description, creds.Description) + s.Require().Equal(credParams.Endpoint, creds.Endpoint.Name) + s.Require().Equal(credParams.AuthType, creds.AuthType) +} + +func (s *GiteaTestSuite) TestCreateCredentialsFailsOnDuplicateCredentials() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + testUser := garmTesting.CreateGARMTestUser(ctx, "testuser", s.db, s.T()) + testUserCtx := auth.PopulateContext(context.Background(), testUser, nil) + + credParams := params.CreateGiteaCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: s.giteaEndpoint.Name, + AuthType: params.ForgeAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test", + }, + } + + _, err := s.db.CreateGiteaCredentials(ctx, credParams) + s.Require().NoError(err) + + // Creating creds with the same parameters should fail for the same user. + _, err = s.db.CreateGiteaCredentials(ctx, credParams) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrDuplicateEntity) + + // Creating creds with the same parameters should work for different users. + _, err = s.db.CreateGiteaCredentials(testUserCtx, credParams) + s.Require().NoError(err) +} + +func (s *GiteaTestSuite) TestNormalUsersCanOnlySeeTheirOwnCredentialsAdminCanSeeAll() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + testUser := garmTesting.CreateGARMTestUser(ctx, "testuser1", s.db, s.T()) + testUser2 := garmTesting.CreateGARMTestUser(ctx, "testuser2", s.db, s.T()) + testUserCtx := auth.PopulateContext(context.Background(), testUser, nil) + testUser2Ctx := auth.PopulateContext(context.Background(), testUser2, nil) + + credParams := params.CreateGiteaCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: s.giteaEndpoint.Name, + AuthType: params.ForgeAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test", + }, + } + + creds, err := s.db.CreateGiteaCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + credParams.Name = "test-creds2" + creds2, err := s.db.CreateGiteaCredentials(testUserCtx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds2) + + credParams.Name = "test-creds3" + creds3, err := s.db.CreateGiteaCredentials(testUser2Ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds3) + + credsList, err := s.db.ListGiteaCredentials(ctx) + s.Require().NoError(err) + s.Require().Len(credsList, 3) + + credsList, err = s.db.ListGiteaCredentials(testUserCtx) + s.Require().NoError(err) + s.Require().Len(credsList, 1) + s.Require().Equal("test-creds2", credsList[0].Name) + + credsList, err = s.db.ListGiteaCredentials(testUser2Ctx) + s.Require().NoError(err) + s.Require().Len(credsList, 1) + s.Require().Equal("test-creds3", credsList[0].Name) +} + +func (s *GiteaTestSuite) TestGetGiteaCredentialsFailsWhenCredentialsDontExist() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + _, err := s.db.GetGiteaCredentials(ctx, 1, true) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) + + _, err = s.db.GetGiteaCredentialsByName(ctx, "non-existing", true) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GiteaTestSuite) TestGetGithubCredentialsByNameReturnsOnlyCurrentUserCredentials() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + testUser := garmTesting.CreateGARMTestUser(ctx, "test-user1", s.db, s.T()) + testUserCtx := auth.PopulateContext(context.Background(), testUser, nil) + + credParams := params.CreateGiteaCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: s.giteaEndpoint.Name, + AuthType: params.ForgeAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test", + }, + } + + creds, err := s.db.CreateGiteaCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + creds2, err := s.db.CreateGiteaCredentials(testUserCtx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds2) + + creds2Get, err := s.db.GetGiteaCredentialsByName(testUserCtx, testCredsName, true) + s.Require().NoError(err) + s.Require().NotNil(creds2) + s.Require().Equal(testCredsName, creds2Get.Name) + s.Require().Equal(creds2.ID, creds2Get.ID) + + credsGet, err := s.db.GetGiteaCredentialsByName(ctx, testCredsName, true) + s.Require().NoError(err) + s.Require().NotNil(creds) + s.Require().Equal(testCredsName, credsGet.Name) + s.Require().Equal(creds.ID, credsGet.ID) + + // Admin can get any creds by ID + credsGet, err = s.db.GetGiteaCredentials(ctx, creds2.ID, true) + s.Require().NoError(err) + s.Require().NotNil(creds2) + s.Require().Equal(creds2.ID, credsGet.ID) + + // Normal user cannot get other user creds by ID + _, err = s.db.GetGiteaCredentials(testUserCtx, creds.ID, true) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GiteaTestSuite) TestGetGithubCredentials() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + credParams := params.CreateGiteaCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: s.giteaEndpoint.Name, + AuthType: params.ForgeAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test", + }, + } + + creds, err := s.db.CreateGiteaCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + creds2, err := s.db.GetGiteaCredentialsByName(ctx, testCredsName, true) + s.Require().NoError(err) + s.Require().NotNil(creds2) + s.Require().Equal(creds.Name, creds2.Name) + s.Require().Equal(creds.ID, creds2.ID) + + creds2, err = s.db.GetGiteaCredentials(ctx, creds.ID, true) + s.Require().NoError(err) + s.Require().NotNil(creds2) + s.Require().Equal(creds.Name, creds2.Name) + s.Require().Equal(creds.ID, creds2.ID) +} + +func (s *GiteaTestSuite) TestDeleteGiteaCredentials() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + credParams := params.CreateGiteaCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: s.giteaEndpoint.Name, + AuthType: params.ForgeAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test", + }, + } + + creds, err := s.db.CreateGiteaCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + err = s.db.DeleteGiteaCredentials(ctx, creds.ID) + s.Require().NoError(err) + + _, err = s.db.GetGiteaCredentials(ctx, creds.ID, true) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GiteaTestSuite) TestDeleteGiteaCredentialsByNonAdminUser() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + testUser := garmTesting.CreateGARMTestUser(ctx, "test-user4", s.db, s.T()) + testUserCtx := auth.PopulateContext(context.Background(), testUser, nil) + + credParams := params.CreateGiteaCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: s.giteaEndpoint.Name, + AuthType: params.ForgeAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test-creds4", + }, + } + + // Create creds as admin + creds, err := s.db.CreateGiteaCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + // Deleting non existent creds will return a nil error. For the test user + // the creds created by the admin should not be visible, which leads to not found + // which in turn returns no error. + err = s.db.DeleteGiteaCredentials(testUserCtx, creds.ID) + s.Require().NoError(err) + + // Check that the creds created by the admin are still there. + credsGet, err := s.db.GetGiteaCredentials(ctx, creds.ID, true) + s.Require().NoError(err) + s.Require().NotNil(credsGet) + s.Require().Equal(creds.ID, credsGet.ID) + + // Create the same creds with the test user. + creds2, err := s.db.CreateGiteaCredentials(testUserCtx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds2) + + // Remove creds created by test user. + err = s.db.DeleteGiteaCredentials(testUserCtx, creds2.ID) + s.Require().NoError(err) + + // The creds created by the test user should be gone. + _, err = s.db.GetGiteaCredentials(testUserCtx, creds2.ID, true) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GiteaTestSuite) TestDeleteCredentialsFailsIfReposOrgsOrEntitiesUseIt() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + credParams := params.CreateGiteaCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: s.giteaEndpoint.Name, + AuthType: params.ForgeAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test", + }, + } + + creds, err := s.db.CreateGiteaCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + repo, err := s.db.CreateRepository(ctx, "test-owner", "test-repo", creds, "superSecret@123BlaBla", params.PoolBalancerTypeRoundRobin) + s.Require().NoError(err) + s.Require().NotNil(repo) + + err = s.db.DeleteGiteaCredentials(ctx, creds.ID) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrBadRequest) + + err = s.db.DeleteRepository(ctx, repo.ID) + s.Require().NoError(err) + + org, err := s.db.CreateOrganization(ctx, "test-org", creds, "superSecret@123BlaBla", params.PoolBalancerTypeRoundRobin) + s.Require().NoError(err) + s.Require().NotNil(org) + + err = s.db.DeleteGiteaCredentials(ctx, creds.ID) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrBadRequest) + + err = s.db.DeleteOrganization(ctx, org.ID) + s.Require().NoError(err) + + enterprise, err := s.db.CreateEnterprise(ctx, "test-enterprise", creds, "superSecret@123BlaBla", params.PoolBalancerTypeRoundRobin) + s.Require().ErrorIs(err, runnerErrors.ErrBadRequest) + s.Require().Equal(params.Enterprise{}, enterprise) + + err = s.db.DeleteGiteaCredentials(ctx, creds.ID) + s.Require().NoError(err) + + _, err = s.db.GetGiteaCredentials(ctx, creds.ID, true) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GiteaTestSuite) TestUpdateCredentials() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + credParams := params.CreateGiteaCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: s.giteaEndpoint.Name, + AuthType: params.ForgeAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test", + }, + } + + creds, err := s.db.CreateGiteaCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + newDescription := "just a description" + newName := "new-name" + newToken := "new-token" + updateCredParams := params.UpdateGiteaCredentialsParams{ + Description: &newDescription, + Name: &newName, + PAT: ¶ms.GithubPAT{ + OAuth2Token: newToken, + }, + } + + updatedCreds, err := s.db.UpdateGiteaCredentials(ctx, creds.ID, updateCredParams) + s.Require().NoError(err) + s.Require().NotNil(updatedCreds) + s.Require().Equal(newDescription, updatedCreds.Description) + s.Require().Equal(newName, updatedCreds.Name) +} + +func (s *GiteaTestSuite) TestUpdateCredentialsFailsForNonExistingCredentials() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + updateCredParams := params.UpdateGiteaCredentialsParams{ + Description: nil, + } + + _, err := s.db.UpdateGiteaCredentials(ctx, 1, updateCredParams) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GiteaTestSuite) TestUpdateCredentialsFailsIfCredentialsAreOwnedByNonAdminUser() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + testUser := garmTesting.CreateGARMTestUser(ctx, "test-user5", s.db, s.T()) + testUserCtx := auth.PopulateContext(context.Background(), testUser, nil) + + credParams := params.CreateGiteaCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: s.giteaEndpoint.Name, + AuthType: params.ForgeAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test-creds5", + }, + } + + creds, err := s.db.CreateGiteaCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + newDescription := "new params desc" + updateCredParams := params.UpdateGiteaCredentialsParams{ + Description: &newDescription, + } + + _, err = s.db.UpdateGiteaCredentials(testUserCtx, creds.ID, updateCredParams) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GiteaTestSuite) TestAdminUserCanUpdateAnyGiteaCredentials() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + testUser := garmTesting.CreateGARMTestUser(ctx, "test-user5", s.db, s.T()) + testUserCtx := auth.PopulateContext(context.Background(), testUser, nil) + + credParams := params.CreateGiteaCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: s.giteaEndpoint.Name, + AuthType: params.ForgeAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test-creds5", + }, + } + + creds, err := s.db.CreateGiteaCredentials(testUserCtx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + newDescription := "another new description" + updateCredParams := params.UpdateGiteaCredentialsParams{ + Description: &newDescription, + } + + newCreds, err := s.db.UpdateGiteaCredentials(ctx, creds.ID, updateCredParams) + s.Require().NoError(err) + s.Require().Equal(newDescription, newCreds.Description) +} + +func (s *GiteaTestSuite) TestDeleteCredentialsWithOrgsOrReposFails() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + credParams := params.CreateGiteaCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: s.giteaEndpoint.Name, + AuthType: params.ForgeAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test-creds5", + }, + } + + creds, err := s.db.CreateGiteaCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + repo, err := s.db.CreateRepository(ctx, "test-owner", "test-repo", creds, "superSecret@123BlaBla", params.PoolBalancerTypeRoundRobin) + s.Require().NoError(err) + s.Require().NotNil(repo) + + err = s.db.DeleteGiteaCredentials(ctx, creds.ID) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrBadRequest) + + err = s.db.DeleteRepository(ctx, repo.ID) + s.Require().NoError(err) + + org, err := s.db.CreateOrganization(ctx, "test-org", creds, "superSecret@123BlaBla", params.PoolBalancerTypeRoundRobin) + s.Require().NoError(err) + s.Require().NotNil(org) + + err = s.db.DeleteGiteaCredentials(ctx, creds.ID) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrBadRequest) + + err = s.db.DeleteOrganization(ctx, org.ID) + s.Require().NoError(err) + + err = s.db.DeleteGiteaCredentials(ctx, creds.ID) + s.Require().NoError(err) + + _, err = s.db.GetGiteaCredentials(ctx, creds.ID, true) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GiteaTestSuite) TestDeleteGiteaEndpointFailsWithOrgsReposOrCredentials() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + endpointParams := params.CreateGiteaEndpointParams{ + Name: "deleteme", + Description: testEndpointDescription, + APIBaseURL: testAPIBaseURL, + BaseURL: testBaseURL, + } + + ep, err := s.db.CreateGiteaEndpoint(ctx, endpointParams) + s.Require().NoError(err) + s.Require().NotNil(ep) + + credParams := params.CreateGiteaCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: ep.Name, + AuthType: params.ForgeAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test-creds5", + }, + } + + creds, err := s.db.CreateGiteaCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + repo, err := s.db.CreateRepository(ctx, "test-owner", "test-repo", creds, "superSecret@123BlaBla", params.PoolBalancerTypeRoundRobin) + s.Require().NoError(err) + s.Require().NotNil(repo) + + badRequest := &runnerErrors.BadRequestError{} + err = s.db.DeleteGiteaEndpoint(ctx, ep.Name) + s.Require().Error(err) + s.Require().ErrorAs(err, &badRequest) + + err = s.db.DeleteRepository(ctx, repo.ID) + s.Require().NoError(err) + + org, err := s.db.CreateOrganization(ctx, "test-org", creds, "superSecret@123BlaBla", params.PoolBalancerTypeRoundRobin) + s.Require().NoError(err) + s.Require().NotNil(org) + + err = s.db.DeleteGiteaEndpoint(ctx, ep.Name) + s.Require().Error(err) + s.Require().ErrorAs(err, &badRequest) + + err = s.db.DeleteOrganization(ctx, org.ID) + s.Require().NoError(err) + + err = s.db.DeleteGiteaCredentials(ctx, creds.ID) + s.Require().NoError(err) + + err = s.db.DeleteGiteaEndpoint(ctx, ep.Name) + s.Require().NoError(err) + + _, err = s.db.GetGiteaEndpoint(ctx, ep.Name) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GiteaTestSuite) TestListGiteaEndpoints() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + createEpParams := params.CreateGiteaEndpointParams{ + Name: "deleteme", + Description: testEndpointDescription, + APIBaseURL: testAPIBaseURL, + BaseURL: testBaseURL, + } + + _, err := s.db.CreateGiteaEndpoint(ctx, createEpParams) + s.Require().NoError(err) + + endpoints, err := s.db.ListGiteaEndpoints(ctx) + s.Require().NoError(err) + s.Require().Len(endpoints, 2) +} + +func TestGiteaTestSuite(t *testing.T) { + suite.Run(t, new(GiteaTestSuite)) +} diff --git a/database/sql/github.go b/database/sql/github.go index 22e357bd9..8dd20225a 100644 --- a/database/sql/github.go +++ b/database/sql/github.go @@ -17,12 +17,10 @@ package sql import ( "context" - "github.com/google/uuid" "github.com/pkg/errors" "gorm.io/gorm" runnerErrors "github.com/cloudbase/garm-provider-common/errors" - "github.com/cloudbase/garm-provider-common/util" "github.com/cloudbase/garm/auth" "github.com/cloudbase/garm/database/common" "github.com/cloudbase/garm/params" @@ -32,89 +30,7 @@ const ( defaultGithubEndpoint string = "github.com" ) -func (s *sqlDatabase) sqlToCommonGithubCredentials(creds GithubCredentials) (params.GithubCredentials, error) { - if len(creds.Payload) == 0 { - return params.GithubCredentials{}, errors.New("empty credentials payload") - } - data, err := util.Unseal(creds.Payload, []byte(s.cfg.Passphrase)) - if err != nil { - return params.GithubCredentials{}, errors.Wrap(err, "unsealing credentials") - } - - ep, err := s.sqlToCommonGithubEndpoint(creds.Endpoint) - if err != nil { - return params.GithubCredentials{}, errors.Wrap(err, "converting github endpoint") - } - - commonCreds := params.GithubCredentials{ - ID: creds.ID, - Name: creds.Name, - Description: creds.Description, - APIBaseURL: creds.Endpoint.APIBaseURL, - BaseURL: creds.Endpoint.BaseURL, - UploadBaseURL: creds.Endpoint.UploadBaseURL, - CABundle: creds.Endpoint.CACertBundle, - AuthType: creds.AuthType, - CreatedAt: creds.CreatedAt, - UpdatedAt: creds.UpdatedAt, - Endpoint: ep, - CredentialsPayload: data, - } - - for _, repo := range creds.Repositories { - commonRepo, err := s.sqlToCommonRepository(repo, false) - if err != nil { - return params.GithubCredentials{}, errors.Wrap(err, "converting github repository") - } - commonCreds.Repositories = append(commonCreds.Repositories, commonRepo) - } - - for _, org := range creds.Organizations { - commonOrg, err := s.sqlToCommonOrganization(org, false) - if err != nil { - return params.GithubCredentials{}, errors.Wrap(err, "converting github organization") - } - commonCreds.Organizations = append(commonCreds.Organizations, commonOrg) - } - - for _, ent := range creds.Enterprises { - commonEnt, err := s.sqlToCommonEnterprise(ent, false) - if err != nil { - return params.GithubCredentials{}, errors.Wrapf(err, "converting github enterprise: %s", ent.Name) - } - commonCreds.Enterprises = append(commonCreds.Enterprises, commonEnt) - } - - return commonCreds, nil -} - -func (s *sqlDatabase) sqlToCommonGithubEndpoint(ep GithubEndpoint) (params.GithubEndpoint, error) { - return params.GithubEndpoint{ - Name: ep.Name, - Description: ep.Description, - APIBaseURL: ep.APIBaseURL, - BaseURL: ep.BaseURL, - UploadBaseURL: ep.UploadBaseURL, - CACertBundle: ep.CACertBundle, - CreatedAt: ep.CreatedAt, - UpdatedAt: ep.UpdatedAt, - }, nil -} - -func getUIDFromContext(ctx context.Context) (uuid.UUID, error) { - userID := auth.UserID(ctx) - if userID == "" { - return uuid.Nil, errors.Wrap(runnerErrors.ErrUnauthorized, "getting UID from context") - } - - asUUID, err := uuid.Parse(userID) - if err != nil { - return uuid.Nil, errors.Wrap(runnerErrors.ErrUnauthorized, "parsing UID from context") - } - return asUUID, nil -} - -func (s *sqlDatabase) CreateGithubEndpoint(_ context.Context, param params.CreateGithubEndpointParams) (ghEndpoint params.GithubEndpoint, err error) { +func (s *sqlDatabase) CreateGithubEndpoint(_ context.Context, param params.CreateGithubEndpointParams) (ghEndpoint params.ForgeEndpoint, err error) { defer func() { if err == nil { s.sendNotify(common.GithubEndpointEntityType, common.CreateOperation, ghEndpoint) @@ -132,6 +48,7 @@ func (s *sqlDatabase) CreateGithubEndpoint(_ context.Context, param params.Creat BaseURL: param.BaseURL, UploadBaseURL: param.UploadBaseURL, CACertBundle: param.CACertBundle, + EndpointType: params.GithubEndpointType, } if err := tx.Create(&endpoint).Error; err != nil { @@ -140,23 +57,23 @@ func (s *sqlDatabase) CreateGithubEndpoint(_ context.Context, param params.Creat return nil }) if err != nil { - return params.GithubEndpoint{}, errors.Wrap(err, "creating github endpoint") + return params.ForgeEndpoint{}, errors.Wrap(err, "creating github endpoint") } ghEndpoint, err = s.sqlToCommonGithubEndpoint(endpoint) if err != nil { - return params.GithubEndpoint{}, errors.Wrap(err, "converting github endpoint") + return params.ForgeEndpoint{}, errors.Wrap(err, "converting github endpoint") } return ghEndpoint, nil } -func (s *sqlDatabase) ListGithubEndpoints(_ context.Context) ([]params.GithubEndpoint, error) { +func (s *sqlDatabase) ListGithubEndpoints(_ context.Context) ([]params.ForgeEndpoint, error) { var endpoints []GithubEndpoint - err := s.conn.Find(&endpoints).Error + err := s.conn.Where("endpoint_type = ?", params.GithubEndpointType).Find(&endpoints).Error if err != nil { return nil, errors.Wrap(err, "fetching github endpoints") } - var ret []params.GithubEndpoint + var ret []params.ForgeEndpoint for _, ep := range endpoints { commonEp, err := s.sqlToCommonGithubEndpoint(ep) if err != nil { @@ -167,9 +84,9 @@ func (s *sqlDatabase) ListGithubEndpoints(_ context.Context) ([]params.GithubEnd return ret, nil } -func (s *sqlDatabase) UpdateGithubEndpoint(_ context.Context, name string, param params.UpdateGithubEndpointParams) (ghEndpoint params.GithubEndpoint, err error) { +func (s *sqlDatabase) UpdateGithubEndpoint(_ context.Context, name string, param params.UpdateGithubEndpointParams) (ghEndpoint params.ForgeEndpoint, err error) { if name == defaultGithubEndpoint { - return params.GithubEndpoint{}, errors.Wrap(runnerErrors.ErrBadRequest, "cannot update default github endpoint") + return params.ForgeEndpoint{}, errors.Wrap(runnerErrors.ErrBadRequest, "cannot update default github endpoint") } defer func() { @@ -179,7 +96,7 @@ func (s *sqlDatabase) UpdateGithubEndpoint(_ context.Context, name string, param }() var endpoint GithubEndpoint err = s.conn.Transaction(func(tx *gorm.DB) error { - if err := tx.Where("name = ?", name).First(&endpoint).Error; err != nil { + if err := tx.Where("name = ? and endpoint_type = ?", name, params.GithubEndpointType).First(&endpoint).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.Wrap(runnerErrors.ErrNotFound, "github endpoint not found") } @@ -212,24 +129,24 @@ func (s *sqlDatabase) UpdateGithubEndpoint(_ context.Context, name string, param return nil }) if err != nil { - return params.GithubEndpoint{}, errors.Wrap(err, "updating github endpoint") + return params.ForgeEndpoint{}, errors.Wrap(err, "updating github endpoint") } ghEndpoint, err = s.sqlToCommonGithubEndpoint(endpoint) if err != nil { - return params.GithubEndpoint{}, errors.Wrap(err, "converting github endpoint") + return params.ForgeEndpoint{}, errors.Wrap(err, "converting github endpoint") } return ghEndpoint, nil } -func (s *sqlDatabase) GetGithubEndpoint(_ context.Context, name string) (params.GithubEndpoint, error) { +func (s *sqlDatabase) GetGithubEndpoint(_ context.Context, name string) (params.ForgeEndpoint, error) { var endpoint GithubEndpoint - err := s.conn.Where("name = ?", name).First(&endpoint).Error + err := s.conn.Where("name = ? and endpoint_type = ?", name, params.GithubEndpointType).First(&endpoint).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return params.GithubEndpoint{}, errors.Wrap(runnerErrors.ErrNotFound, "github endpoint not found") + return params.ForgeEndpoint{}, errors.Wrap(runnerErrors.ErrNotFound, "github endpoint not found") } - return params.GithubEndpoint{}, errors.Wrap(err, "fetching github endpoint") + return params.ForgeEndpoint{}, errors.Wrap(err, "fetching github endpoint") } return s.sqlToCommonGithubEndpoint(endpoint) @@ -242,12 +159,12 @@ func (s *sqlDatabase) DeleteGithubEndpoint(_ context.Context, name string) (err defer func() { if err == nil { - s.sendNotify(common.GithubEndpointEntityType, common.DeleteOperation, params.GithubEndpoint{Name: name}) + s.sendNotify(common.GithubEndpointEntityType, common.DeleteOperation, params.ForgeEndpoint{Name: name}) } }() err = s.conn.Transaction(func(tx *gorm.DB) error { var endpoint GithubEndpoint - if err := tx.Where("name = ?", name).First(&endpoint).Error; err != nil { + if err := tx.Where("name = ? and endpoint_type = ?", name, params.GithubEndpointType).First(&endpoint).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil } @@ -283,7 +200,7 @@ func (s *sqlDatabase) DeleteGithubEndpoint(_ context.Context, name string) (err } if credsCount > 0 || repoCnt > 0 || orgCnt > 0 || entCnt > 0 { - return errors.New("cannot delete endpoint with associated entities") + return runnerErrors.NewBadRequestError("cannot delete endpoint with associated entities") } if err := tx.Unscoped().Delete(&endpoint).Error; err != nil { @@ -297,13 +214,13 @@ func (s *sqlDatabase) DeleteGithubEndpoint(_ context.Context, name string) (err return nil } -func (s *sqlDatabase) CreateGithubCredentials(ctx context.Context, param params.CreateGithubCredentialsParams) (ghCreds params.GithubCredentials, err error) { +func (s *sqlDatabase) CreateGithubCredentials(ctx context.Context, param params.CreateGithubCredentialsParams) (ghCreds params.ForgeCredentials, err error) { userID, err := getUIDFromContext(ctx) if err != nil { - return params.GithubCredentials{}, errors.Wrap(err, "creating github credentials") + return params.ForgeCredentials{}, errors.Wrap(err, "creating github credentials") } if param.Endpoint == "" { - return params.GithubCredentials{}, errors.Wrap(runnerErrors.ErrBadRequest, "endpoint name is required") + return params.ForgeCredentials{}, errors.Wrap(runnerErrors.ErrBadRequest, "endpoint name is required") } defer func() { @@ -314,7 +231,7 @@ func (s *sqlDatabase) CreateGithubCredentials(ctx context.Context, param params. var creds GithubCredentials err = s.conn.Transaction(func(tx *gorm.DB) error { var endpoint GithubEndpoint - if err := tx.Where("name = ?", param.Endpoint).First(&endpoint).Error; err != nil { + if err := tx.Where("name = ? and endpoint_type = ?", param.Endpoint, params.GithubEndpointType).First(&endpoint).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.Wrap(runnerErrors.ErrNotFound, "github endpoint not found") } @@ -328,9 +245,9 @@ func (s *sqlDatabase) CreateGithubCredentials(ctx context.Context, param params. var data []byte var err error switch param.AuthType { - case params.GithubAuthTypePAT: + case params.ForgeAuthTypePAT: data, err = s.marshalAndSeal(param.PAT) - case params.GithubAuthTypeApp: + case params.ForgeAuthTypeApp: data, err = s.marshalAndSeal(param.App) default: return errors.Wrap(runnerErrors.ErrBadRequest, "invalid auth type") @@ -357,11 +274,11 @@ func (s *sqlDatabase) CreateGithubCredentials(ctx context.Context, param params. return nil }) if err != nil { - return params.GithubCredentials{}, errors.Wrap(err, "creating github credentials") + return params.ForgeCredentials{}, errors.Wrap(err, "creating github credentials") } - ghCreds, err = s.sqlToCommonGithubCredentials(creds) + ghCreds, err = s.sqlToCommonForgeCredentials(creds) if err != nil { - return params.GithubCredentials{}, errors.Wrap(err, "converting github credentials") + return params.ForgeCredentials{}, errors.Wrap(err, "converting github credentials") } return ghCreds, nil } @@ -373,8 +290,11 @@ func (s *sqlDatabase) getGithubCredentialsByName(ctx context.Context, tx *gorm.D if detailed { q = q. Preload("Repositories"). + Preload("Repositories.Credentials"). Preload("Organizations"). - Preload("Enterprises") + Preload("Organizations.Credentials"). + Preload("Enterprises"). + Preload("Enterprises.Credentials") } userID, err := getUIDFromContext(ctx) @@ -394,30 +314,32 @@ func (s *sqlDatabase) getGithubCredentialsByName(ctx context.Context, tx *gorm.D return creds, nil } -func (s *sqlDatabase) GetGithubCredentialsByName(ctx context.Context, name string, detailed bool) (params.GithubCredentials, error) { +func (s *sqlDatabase) GetGithubCredentialsByName(ctx context.Context, name string, detailed bool) (params.ForgeCredentials, error) { creds, err := s.getGithubCredentialsByName(ctx, s.conn, name, detailed) if err != nil { - return params.GithubCredentials{}, errors.Wrap(err, "fetching github credentials") + return params.ForgeCredentials{}, errors.Wrap(err, "fetching github credentials") } - - return s.sqlToCommonGithubCredentials(creds) + return s.sqlToCommonForgeCredentials(creds) } -func (s *sqlDatabase) GetGithubCredentials(ctx context.Context, id uint, detailed bool) (params.GithubCredentials, error) { +func (s *sqlDatabase) GetGithubCredentials(ctx context.Context, id uint, detailed bool) (params.ForgeCredentials, error) { var creds GithubCredentials q := s.conn.Preload("Endpoint") if detailed { q = q. Preload("Repositories"). + Preload("Repositories.Credentials"). Preload("Organizations"). - Preload("Enterprises") + Preload("Organizations.Credentials"). + Preload("Enterprises"). + Preload("Enterprises.Credentials") } if !auth.IsAdmin(ctx) { userID, err := getUIDFromContext(ctx) if err != nil { - return params.GithubCredentials{}, errors.Wrap(err, "fetching github credentials") + return params.ForgeCredentials{}, errors.Wrap(err, "fetching github credentials") } q = q.Where("user_id = ?", userID) } @@ -425,15 +347,15 @@ func (s *sqlDatabase) GetGithubCredentials(ctx context.Context, id uint, detaile err := q.Where("id = ?", id).First(&creds).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return params.GithubCredentials{}, errors.Wrap(runnerErrors.ErrNotFound, "github credentials not found") + return params.ForgeCredentials{}, errors.Wrap(runnerErrors.ErrNotFound, "github credentials not found") } - return params.GithubCredentials{}, errors.Wrap(err, "fetching github credentials") + return params.ForgeCredentials{}, errors.Wrap(err, "fetching github credentials") } - return s.sqlToCommonGithubCredentials(creds) + return s.sqlToCommonForgeCredentials(creds) } -func (s *sqlDatabase) ListGithubCredentials(ctx context.Context) ([]params.GithubCredentials, error) { +func (s *sqlDatabase) ListGithubCredentials(ctx context.Context) ([]params.ForgeCredentials, error) { q := s.conn.Preload("Endpoint") if !auth.IsAdmin(ctx) { userID, err := getUIDFromContext(ctx) @@ -449,9 +371,9 @@ func (s *sqlDatabase) ListGithubCredentials(ctx context.Context) ([]params.Githu return nil, errors.Wrap(err, "fetching github credentials") } - var ret []params.GithubCredentials + var ret []params.ForgeCredentials for _, c := range creds { - commonCreds, err := s.sqlToCommonGithubCredentials(c) + commonCreds, err := s.sqlToCommonForgeCredentials(c) if err != nil { return nil, errors.Wrap(err, "converting github credentials") } @@ -460,7 +382,7 @@ func (s *sqlDatabase) ListGithubCredentials(ctx context.Context) ([]params.Githu return ret, nil } -func (s *sqlDatabase) UpdateGithubCredentials(ctx context.Context, id uint, param params.UpdateGithubCredentialsParams) (ghCreds params.GithubCredentials, err error) { +func (s *sqlDatabase) UpdateGithubCredentials(ctx context.Context, id uint, param params.UpdateGithubCredentialsParams) (ghCreds params.ForgeCredentials, err error) { defer func() { if err == nil { s.sendNotify(common.GithubCredentialsEntityType, common.UpdateOperation, ghCreds) @@ -494,7 +416,7 @@ func (s *sqlDatabase) UpdateGithubCredentials(ctx context.Context, id uint, para var data []byte var err error switch creds.AuthType { - case params.GithubAuthTypePAT: + case params.ForgeAuthTypePAT: if param.PAT != nil { data, err = s.marshalAndSeal(param.PAT) } @@ -502,7 +424,7 @@ func (s *sqlDatabase) UpdateGithubCredentials(ctx context.Context, id uint, para if param.App != nil { return errors.Wrap(runnerErrors.ErrBadRequest, "cannot update app credentials for PAT") } - case params.GithubAuthTypeApp: + case params.ForgeAuthTypeApp: if param.App != nil { data, err = s.marshalAndSeal(param.App) } @@ -529,12 +451,12 @@ func (s *sqlDatabase) UpdateGithubCredentials(ctx context.Context, id uint, para return nil }) if err != nil { - return params.GithubCredentials{}, errors.Wrap(err, "updating github credentials") + return params.ForgeCredentials{}, errors.Wrap(err, "updating github credentials") } - ghCreds, err = s.sqlToCommonGithubCredentials(creds) + ghCreds, err = s.sqlToCommonForgeCredentials(creds) if err != nil { - return params.GithubCredentials{}, errors.Wrap(err, "converting github credentials") + return params.ForgeCredentials{}, errors.Wrap(err, "converting github credentials") } return ghCreds, nil } @@ -543,7 +465,7 @@ func (s *sqlDatabase) DeleteGithubCredentials(ctx context.Context, id uint) (err var name string defer func() { if err == nil { - s.sendNotify(common.GithubCredentialsEntityType, common.DeleteOperation, params.GithubCredentials{ID: id, Name: name}) + s.sendNotify(common.GithubCredentialsEntityType, common.DeleteOperation, params.ForgeCredentials{ID: id, Name: name}) } }() err = s.conn.Transaction(func(tx *gorm.DB) error { diff --git a/database/sql/github_test.go b/database/sql/github_test.go index 9d53569a3..7b99d5e28 100644 --- a/database/sql/github_test.go +++ b/database/sql/github_test.go @@ -33,13 +33,14 @@ import ( ) const ( - testUploadBaseURL string = "https://uploads.example.com" - testBaseURL string = "https://example.com" - testAPIBaseURL string = "https://api.example.com" - testEndpointName string = "test-endpoint" - testEndpointDescription string = "test description" - testCredsName string = "test-creds" - testCredsDescription string = "test creds" + testUploadBaseURL string = "https://uploads.example.com" + testBaseURL string = "https://example.com" + testAPIBaseURL string = "https://api.example.com" + testEndpointName string = "test-endpoint" + alternetTestEndpointName string = "test-endpoint-alternate" + testEndpointDescription string = "test description" + testCredsName string = "test-creds" + testCredsDescription string = "test creds" ) type GithubTestSuite struct { @@ -266,7 +267,7 @@ func (s *GithubTestSuite) TestCreateCredentials() { Name: testCredsName, Description: testCredsDescription, Endpoint: defaultGithubEndpoint, - AuthType: params.GithubAuthTypePAT, + AuthType: params.ForgeAuthTypePAT, PAT: params.GithubPAT{ OAuth2Token: "test", }, @@ -290,7 +291,7 @@ func (s *GithubTestSuite) TestCreateCredentialsFailsOnDuplicateCredentials() { Name: testCredsName, Description: testCredsDescription, Endpoint: defaultGithubEndpoint, - AuthType: params.GithubAuthTypePAT, + AuthType: params.ForgeAuthTypePAT, PAT: params.GithubPAT{ OAuth2Token: "test", }, @@ -320,7 +321,7 @@ func (s *GithubTestSuite) TestNormalUsersCanOnlySeeTheirOwnCredentialsAdminCanSe Name: testCredsName, Description: testCredsDescription, Endpoint: defaultGithubEndpoint, - AuthType: params.GithubAuthTypePAT, + AuthType: params.ForgeAuthTypePAT, PAT: params.GithubPAT{ OAuth2Token: "test", }, @@ -376,7 +377,7 @@ func (s *GithubTestSuite) TestGetGithubCredentialsByNameReturnsOnlyCurrentUserCr Name: testCredsName, Description: testCredsDescription, Endpoint: defaultGithubEndpoint, - AuthType: params.GithubAuthTypePAT, + AuthType: params.ForgeAuthTypePAT, PAT: params.GithubPAT{ OAuth2Token: "test", }, @@ -421,7 +422,7 @@ func (s *GithubTestSuite) TestGetGithubCredentials() { Name: testCredsName, Description: testCredsDescription, Endpoint: defaultGithubEndpoint, - AuthType: params.GithubAuthTypePAT, + AuthType: params.ForgeAuthTypePAT, PAT: params.GithubPAT{ OAuth2Token: "test", }, @@ -451,7 +452,7 @@ func (s *GithubTestSuite) TestDeleteGithubCredentials() { Name: testCredsName, Description: testCredsDescription, Endpoint: defaultGithubEndpoint, - AuthType: params.GithubAuthTypePAT, + AuthType: params.ForgeAuthTypePAT, PAT: params.GithubPAT{ OAuth2Token: "test", }, @@ -478,7 +479,7 @@ func (s *GithubTestSuite) TestDeleteGithubCredentialsByNonAdminUser() { Name: testCredsName, Description: testCredsDescription, Endpoint: defaultGithubEndpoint, - AuthType: params.GithubAuthTypePAT, + AuthType: params.ForgeAuthTypePAT, PAT: params.GithubPAT{ OAuth2Token: "test-creds4", }, @@ -523,7 +524,7 @@ func (s *GithubTestSuite) TestDeleteCredentialsFailsIfReposOrgsOrEntitiesUseIt() Name: testCredsName, Description: testCredsDescription, Endpoint: defaultGithubEndpoint, - AuthType: params.GithubAuthTypePAT, + AuthType: params.ForgeAuthTypePAT, PAT: params.GithubPAT{ OAuth2Token: "test", }, @@ -533,7 +534,7 @@ func (s *GithubTestSuite) TestDeleteCredentialsFailsIfReposOrgsOrEntitiesUseIt() s.Require().NoError(err) s.Require().NotNil(creds) - repo, err := s.db.CreateRepository(ctx, "test-owner", "test-repo", creds.Name, "superSecret@123BlaBla", params.PoolBalancerTypeRoundRobin) + repo, err := s.db.CreateRepository(ctx, "test-owner", "test-repo", creds, "superSecret@123BlaBla", params.PoolBalancerTypeRoundRobin) s.Require().NoError(err) s.Require().NotNil(repo) @@ -544,7 +545,7 @@ func (s *GithubTestSuite) TestDeleteCredentialsFailsIfReposOrgsOrEntitiesUseIt() err = s.db.DeleteRepository(ctx, repo.ID) s.Require().NoError(err) - org, err := s.db.CreateOrganization(ctx, "test-org", creds.Name, "superSecret@123BlaBla", params.PoolBalancerTypeRoundRobin) + org, err := s.db.CreateOrganization(ctx, "test-org", creds, "superSecret@123BlaBla", params.PoolBalancerTypeRoundRobin) s.Require().NoError(err) s.Require().NotNil(org) @@ -555,7 +556,7 @@ func (s *GithubTestSuite) TestDeleteCredentialsFailsIfReposOrgsOrEntitiesUseIt() err = s.db.DeleteOrganization(ctx, org.ID) s.Require().NoError(err) - enterprise, err := s.db.CreateEnterprise(ctx, "test-enterprise", creds.Name, "superSecret@123BlaBla", params.PoolBalancerTypeRoundRobin) + enterprise, err := s.db.CreateEnterprise(ctx, "test-enterprise", creds, "superSecret@123BlaBla", params.PoolBalancerTypeRoundRobin) s.Require().NoError(err) s.Require().NotNil(enterprise) @@ -581,7 +582,7 @@ func (s *GithubTestSuite) TestUpdateCredentials() { Name: testCredsName, Description: testCredsDescription, Endpoint: defaultGithubEndpoint, - AuthType: params.GithubAuthTypePAT, + AuthType: params.ForgeAuthTypePAT, PAT: params.GithubPAT{ OAuth2Token: "test", }, @@ -616,7 +617,7 @@ func (s *GithubTestSuite) TestUpdateGithubCredentialsFailIfWrongCredentialTypeIs Name: testCredsName, Description: testCredsDescription, Endpoint: defaultGithubEndpoint, - AuthType: params.GithubAuthTypePAT, + AuthType: params.ForgeAuthTypePAT, PAT: params.GithubPAT{ OAuth2Token: "test", }, @@ -643,7 +644,7 @@ func (s *GithubTestSuite) TestUpdateGithubCredentialsFailIfWrongCredentialTypeIs Name: "test-credsApp", Description: "test credsApp", Endpoint: defaultGithubEndpoint, - AuthType: params.GithubAuthTypeApp, + AuthType: params.ForgeAuthTypeApp, App: params.GithubApp{ AppID: 1, InstallationID: 2, @@ -688,7 +689,7 @@ func (s *GithubTestSuite) TestUpdateCredentialsFailsIfCredentialsAreOwnedByNonAd Name: testCredsName, Description: testCredsDescription, Endpoint: defaultGithubEndpoint, - AuthType: params.GithubAuthTypePAT, + AuthType: params.ForgeAuthTypePAT, PAT: params.GithubPAT{ OAuth2Token: "test-creds5", }, @@ -717,7 +718,7 @@ func (s *GithubTestSuite) TestAdminUserCanUpdateAnyGithubCredentials() { Name: testCredsName, Description: testCredsDescription, Endpoint: defaultGithubEndpoint, - AuthType: params.GithubAuthTypePAT, + AuthType: params.ForgeAuthTypePAT, PAT: params.GithubPAT{ OAuth2Token: "test-creds5", }, @@ -737,6 +738,68 @@ func (s *GithubTestSuite) TestAdminUserCanUpdateAnyGithubCredentials() { s.Require().Equal(newDescription, newCreds.Description) } +func (s *GithubTestSuite) TestDeleteGithubEndpointFailsWithOrgsReposOrCredentials() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + endpointParams := params.CreateGithubEndpointParams{ + Name: "deleteme", + Description: testEndpointDescription, + APIBaseURL: testAPIBaseURL, + BaseURL: testBaseURL, + } + + ep, err := s.db.CreateGithubEndpoint(ctx, endpointParams) + s.Require().NoError(err) + s.Require().NotNil(ep) + + credParams := params.CreateGithubCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: ep.Name, + AuthType: params.ForgeAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test-creds5", + }, + } + + creds, err := s.db.CreateGithubCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + repo, err := s.db.CreateRepository(ctx, "test-owner", "test-repo", creds, "superSecret@123BlaBla", params.PoolBalancerTypeRoundRobin) + s.Require().NoError(err) + s.Require().NotNil(repo) + + badRequest := &runnerErrors.BadRequestError{} + err = s.db.DeleteGithubEndpoint(ctx, ep.Name) + s.Require().Error(err) + s.Require().ErrorAs(err, &badRequest) + + err = s.db.DeleteRepository(ctx, repo.ID) + s.Require().NoError(err) + + org, err := s.db.CreateOrganization(ctx, "test-org", creds, "superSecret@123BlaBla", params.PoolBalancerTypeRoundRobin) + s.Require().NoError(err) + s.Require().NotNil(org) + + err = s.db.DeleteGithubEndpoint(ctx, ep.Name) + s.Require().Error(err) + s.Require().ErrorAs(err, &badRequest) + + err = s.db.DeleteOrganization(ctx, org.ID) + s.Require().NoError(err) + + err = s.db.DeleteGithubCredentials(ctx, creds.ID) + s.Require().NoError(err) + + err = s.db.DeleteGithubEndpoint(ctx, ep.Name) + s.Require().NoError(err) + + _, err = s.db.GetGithubEndpoint(ctx, ep.Name) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + func TestGithubTestSuite(t *testing.T) { suite.Run(t, new(GithubTestSuite)) } @@ -836,10 +899,10 @@ func TestCredentialsAndEndpointMigration(t *testing.T) { t.Fatalf("expected ghes-test to be associated with example.com endpoint, got %s", creds[1].Endpoint.Name) } - if creds[0].AuthType != params.GithubAuthTypePAT { + if creds[0].AuthType != params.ForgeAuthTypePAT { t.Fatalf("expected test-creds to have PAT auth type, got %s", creds[0].AuthType) } - if creds[1].AuthType != params.GithubAuthTypeApp { + if creds[1].AuthType != params.ForgeAuthTypeApp { t.Fatalf("expected ghes-test to have App auth type, got %s", creds[1].AuthType) } if len(creds[0].CredentialsPayload) == 0 { diff --git a/database/sql/instances_test.go b/database/sql/instances_test.go index 8610409b4..c70e35dd2 100644 --- a/database/sql/instances_test.go +++ b/database/sql/instances_test.go @@ -84,7 +84,7 @@ func (s *InstancesTestSuite) SetupTest() { creds := garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), githubEndpoint) // create an organization for testing purposes - org, err := s.Store.CreateOrganization(s.adminCtx, "test-org", creds.Name, "test-webhookSecret", params.PoolBalancerTypeRoundRobin) + org, err := s.Store.CreateOrganization(s.adminCtx, "test-org", creds, "test-webhookSecret", params.PoolBalancerTypeRoundRobin) if err != nil { s.FailNow(fmt.Sprintf("failed to create org: %s", err)) } diff --git a/database/sql/jobs.go b/database/sql/jobs.go index b7dda9263..1215e3f37 100644 --- a/database/sql/jobs.go +++ b/database/sql/jobs.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package sql import ( @@ -306,7 +320,7 @@ func (s *sqlDatabase) ListJobsByStatus(_ context.Context, status params.JobStatu } // ListEntityJobsByStatus lists all jobs for a given entity type and id. -func (s *sqlDatabase) ListEntityJobsByStatus(_ context.Context, entityType params.GithubEntityType, entityID string, status params.JobStatus) ([]params.Job, error) { +func (s *sqlDatabase) ListEntityJobsByStatus(_ context.Context, entityType params.ForgeEntityType, entityID string, status params.JobStatus) ([]params.Job, error) { u, err := uuid.Parse(entityID) if err != nil { return nil, err @@ -316,11 +330,11 @@ func (s *sqlDatabase) ListEntityJobsByStatus(_ context.Context, entityType param query := s.conn.Model(&WorkflowJob{}).Preload("Instance").Where("status = ?", status) switch entityType { - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: query = query.Where("org_id = ?", u) - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: query = query.Where("repo_id = ?", u) - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: query = query.Where("enterprise_id = ?", u) } diff --git a/database/sql/models.go b/database/sql/models.go index d6fbb6e93..154fb51da 100644 --- a/database/sql/models.go +++ b/database/sql/models.go @@ -1,4 +1,4 @@ -// Copyright 2022 Cloudbase Solutions SRL +// Copyright 2025 Cloudbase Solutions SRL // // Licensed under the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. You may obtain @@ -46,6 +46,22 @@ func (b *Base) BeforeCreate(_ *gorm.DB) error { return nil } +type ControllerInfo struct { + Base + + ControllerID uuid.UUID + + CallbackURL string + MetadataURL string + WebhookBaseURL string + // MinimumJobAgeBackoff is the minimum time that a job must be in the queue + // before GARM will attempt to allocate a runner to service it. This backoff + // is useful if you have idle runners in various pools that could potentially + // pick up the job. GARM would allow this amount of time for runners to react + // before spinning up a new one and potentially having to scale down later. + MinimumJobAgeBackoff uint +} + type Tag struct { Base @@ -152,11 +168,12 @@ type RepositoryEvent struct { type Repository struct { Base - CredentialsName string - CredentialsID *uint `gorm:"index"` Credentials GithubCredentials `gorm:"foreignKey:CredentialsID;constraint:OnDelete:SET NULL"` + GiteaCredentialsID *uint `gorm:"index"` + GiteaCredentials GiteaCredentials `gorm:"foreignKey:GiteaCredentialsID;constraint:OnDelete:SET NULL"` + Owner string `gorm:"index:idx_owner_nocase,unique,collate:nocase"` Name string `gorm:"index:idx_owner_nocase,unique,collate:nocase"` WebhookSecret []byte @@ -168,7 +185,7 @@ type Repository struct { EndpointName *string `gorm:"index:idx_owner_nocase,unique,collate:nocase"` Endpoint GithubEndpoint `gorm:"foreignKey:EndpointName;constraint:OnDelete:SET NULL"` - Events []RepositoryEvent `gorm:"foreignKey:RepoID;constraint:OnDelete:CASCADE,OnUpdate:CASCADE;"` + Events []*RepositoryEvent `gorm:"foreignKey:RepoID;constraint:OnDelete:CASCADE,OnUpdate:CASCADE;"` } type OrganizationEvent struct { @@ -184,11 +201,12 @@ type OrganizationEvent struct { type Organization struct { Base - CredentialsName string - CredentialsID *uint `gorm:"index"` Credentials GithubCredentials `gorm:"foreignKey:CredentialsID;constraint:OnDelete:SET NULL"` + GiteaCredentialsID *uint `gorm:"index"` + GiteaCredentials GiteaCredentials `gorm:"foreignKey:GiteaCredentialsID;constraint:OnDelete:SET NULL"` + Name string `gorm:"index:idx_org_name_nocase,collate:nocase"` WebhookSecret []byte Pools []Pool `gorm:"foreignKey:OrgID"` @@ -199,7 +217,7 @@ type Organization struct { EndpointName *string `gorm:"index:idx_org_name_nocase,collate:nocase"` Endpoint GithubEndpoint `gorm:"foreignKey:EndpointName;constraint:OnDelete:SET NULL"` - Events []OrganizationEvent `gorm:"foreignKey:OrgID;constraint:OnDelete:CASCADE,OnUpdate:CASCADE;"` + Events []*OrganizationEvent `gorm:"foreignKey:OrgID;constraint:OnDelete:CASCADE,OnUpdate:CASCADE;"` } type EnterpriseEvent struct { @@ -216,8 +234,6 @@ type EnterpriseEvent struct { type Enterprise struct { Base - CredentialsName string - CredentialsID *uint `gorm:"index"` Credentials GithubCredentials `gorm:"foreignKey:CredentialsID;constraint:OnDelete:SET NULL"` @@ -231,7 +247,7 @@ type Enterprise struct { EndpointName *string `gorm:"index:idx_ent_name_nocase,collate:nocase"` Endpoint GithubEndpoint `gorm:"foreignKey:EndpointName;constraint:OnDelete:SET NULL"` - Events []EnterpriseEvent `gorm:"foreignKey:EnterpriseID;constraint:OnDelete:CASCADE,OnUpdate:CASCADE;"` + Events []*EnterpriseEvent `gorm:"foreignKey:EnterpriseID;constraint:OnDelete:CASCADE,OnUpdate:CASCADE;"` } type Address struct { @@ -300,22 +316,6 @@ type User struct { Enabled bool } -type ControllerInfo struct { - Base - - ControllerID uuid.UUID - - CallbackURL string - MetadataURL string - WebhookBaseURL string - // MinimumJobAgeBackoff is the minimum time that a job must be in the queue - // before GARM will attempt to allocate a runner to service it. This backoff - // is useful if you have idle runners in various pools that could potentially - // pick up the job. GARM would allow this amount of time for runners to react - // before spinning up a new one and potentially having to scale down later. - MinimumJobAgeBackoff uint -} - type WorkflowJob struct { // ID is the ID of the job. ID int64 `gorm:"index"` @@ -381,6 +381,8 @@ type GithubEndpoint struct { UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` + EndpointType params.EndpointType `gorm:"index:idx_endpoint_type"` + Description string `gorm:"type:text"` APIBaseURL string `gorm:"type:text collate nocase"` UploadBaseURL string `gorm:"type:text collate nocase"` @@ -395,9 +397,9 @@ type GithubCredentials struct { UserID *uuid.UUID `gorm:"index:idx_github_credentials,unique"` User User `gorm:"foreignKey:UserID"` - Description string `gorm:"type:text"` - AuthType params.GithubAuthType `gorm:"index"` - Payload []byte `gorm:"type:longblob"` + Description string `gorm:"type:text"` + AuthType params.ForgeAuthType `gorm:"index"` + Payload []byte `gorm:"type:longblob"` Endpoint GithubEndpoint `gorm:"foreignKey:EndpointName"` EndpointName *string `gorm:"index"` @@ -406,3 +408,21 @@ type GithubCredentials struct { Organizations []Organization `gorm:"foreignKey:CredentialsID"` Enterprises []Enterprise `gorm:"foreignKey:CredentialsID"` } + +type GiteaCredentials struct { + gorm.Model + + Name string `gorm:"index:idx_gitea_credentials,unique;type:varchar(64) collate nocase"` + UserID *uuid.UUID `gorm:"index:idx_gitea_credentials,unique"` + User User `gorm:"foreignKey:UserID"` + + Description string `gorm:"type:text"` + AuthType params.ForgeAuthType `gorm:"index"` + Payload []byte `gorm:"type:longblob"` + + Endpoint GithubEndpoint `gorm:"foreignKey:EndpointName"` + EndpointName *string `gorm:"index"` + + Repositories []Repository `gorm:"foreignKey:GiteaCredentialsID"` + Organizations []Organization `gorm:"foreignKey:GiteaCredentialsID"` +} diff --git a/database/sql/organizations.go b/database/sql/organizations.go index 07ce32d81..6f8eaa101 100644 --- a/database/sql/organizations.go +++ b/database/sql/organizations.go @@ -29,7 +29,7 @@ import ( "github.com/cloudbase/garm/params" ) -func (s *sqlDatabase) CreateOrganization(ctx context.Context, name, credentialsName, webhookSecret string, poolBalancerType params.PoolBalancerType) (org params.Organization, err error) { +func (s *sqlDatabase) CreateOrganization(ctx context.Context, name string, credentials params.ForgeCredentials, webhookSecret string, poolBalancerType params.PoolBalancerType) (param params.Organization, err error) { if webhookSecret == "" { return params.Organization{}, errors.New("creating org: missing secret") } @@ -40,49 +40,47 @@ func (s *sqlDatabase) CreateOrganization(ctx context.Context, name, credentialsN defer func() { if err == nil { - s.sendNotify(common.OrganizationEntityType, common.CreateOperation, org) + s.sendNotify(common.OrganizationEntityType, common.CreateOperation, param) } }() newOrg := Organization{ Name: name, WebhookSecret: secret, - CredentialsName: credentialsName, PoolBalancerType: poolBalancerType, } err = s.conn.Transaction(func(tx *gorm.DB) error { - creds, err := s.getGithubCredentialsByName(ctx, tx, credentialsName, false) - if err != nil { - return errors.Wrap(err, "creating org") - } - if creds.EndpointName == nil { - return errors.Wrap(runnerErrors.ErrUnprocessable, "credentials have no endpoint") + switch credentials.ForgeType { + case params.GithubEndpointType: + newOrg.CredentialsID = &credentials.ID + case params.GiteaEndpointType: + newOrg.GiteaCredentialsID = &credentials.ID + default: + return errors.Wrap(runnerErrors.ErrBadRequest, "unsupported credentials type") } - newOrg.CredentialsID = &creds.ID - newOrg.CredentialsName = creds.Name - newOrg.EndpointName = creds.EndpointName + newOrg.EndpointName = &credentials.Endpoint.Name q := tx.Create(&newOrg) if q.Error != nil { return errors.Wrap(q.Error, "creating org") } - - newOrg.Credentials = creds - newOrg.Endpoint = creds.Endpoint - return nil }) if err != nil { return params.Organization{}, errors.Wrap(err, "creating org") } - org, err = s.sqlToCommonOrganization(newOrg, true) + org, err := s.getOrgByID(ctx, s.conn, newOrg.ID.String(), "Pools", "Endpoint", "Credentials", "GiteaCredentials", "Credentials.Endpoint", "GiteaCredentials.Endpoint") if err != nil { return params.Organization{}, errors.Wrap(err, "creating org") } - org.WebhookSecret = webhookSecret - return org, nil + param, err = s.sqlToCommonOrganization(org, true) + if err != nil { + return params.Organization{}, errors.Wrap(err, "creating org") + } + + return param, nil } func (s *sqlDatabase) GetOrganization(ctx context.Context, name, endpointName string) (params.Organization, error) { @@ -103,7 +101,9 @@ func (s *sqlDatabase) ListOrganizations(_ context.Context) ([]params.Organizatio var orgs []Organization q := s.conn. Preload("Credentials"). + Preload("GiteaCredentials"). Preload("Credentials.Endpoint"). + Preload("GiteaCredentials.Endpoint"). Preload("Endpoint"). Find(&orgs) if q.Error != nil { @@ -123,7 +123,7 @@ func (s *sqlDatabase) ListOrganizations(_ context.Context) ([]params.Organizatio } func (s *sqlDatabase) DeleteOrganization(ctx context.Context, orgID string) (err error) { - org, err := s.getOrgByID(ctx, s.conn, orgID, "Endpoint", "Credentials", "Credentials.Endpoint") + org, err := s.getOrgByID(ctx, s.conn, orgID, "Endpoint", "Credentials", "Credentials.Endpoint", "GiteaCredentials", "GiteaCredentials.Endpoint") if err != nil { return errors.Wrap(err, "fetching org") } @@ -166,7 +166,6 @@ func (s *sqlDatabase) UpdateOrganization(ctx context.Context, orgID string, para } if param.CredentialsName != "" { - org.CredentialsName = param.CredentialsName creds, err = s.getGithubCredentialsByName(ctx, tx, param.CredentialsName, false) if err != nil { return errors.Wrap(err, "fetching credentials") @@ -204,7 +203,7 @@ func (s *sqlDatabase) UpdateOrganization(ctx context.Context, orgID string, para return params.Organization{}, errors.Wrap(err, "saving org") } - org, err = s.getOrgByID(ctx, s.conn, orgID, "Endpoint", "Credentials", "Credentials.Endpoint") + org, err = s.getOrgByID(ctx, s.conn, orgID, "Endpoint", "Credentials", "Credentials.Endpoint", "GiteaCredentials", "GiteaCredentials.Endpoint") if err != nil { return params.Organization{}, errors.Wrap(err, "updating enterprise") } @@ -216,7 +215,7 @@ func (s *sqlDatabase) UpdateOrganization(ctx context.Context, orgID string, para } func (s *sqlDatabase) GetOrganizationByID(ctx context.Context, orgID string) (params.Organization, error) { - org, err := s.getOrgByID(ctx, s.conn, orgID, "Pools", "Credentials", "Endpoint", "Credentials.Endpoint") + org, err := s.getOrgByID(ctx, s.conn, orgID, "Pools", "Credentials", "Endpoint", "Credentials.Endpoint", "GiteaCredentials", "GiteaCredentials.Endpoint") if err != nil { return params.Organization{}, errors.Wrap(err, "fetching org") } @@ -257,7 +256,9 @@ func (s *sqlDatabase) getOrg(_ context.Context, name, endpointName string) (Orga q := s.conn.Where("name = ? COLLATE NOCASE and endpoint_name = ? COLLATE NOCASE", name, endpointName). Preload("Credentials"). + Preload("GiteaCredentials"). Preload("Credentials.Endpoint"). + Preload("GiteaCredentials.Endpoint"). Preload("Endpoint"). First(&org) if q.Error != nil { diff --git a/database/sql/organizations_test.go b/database/sql/organizations_test.go index b30ea701f..a93ef3728 100644 --- a/database/sql/organizations_test.go +++ b/database/sql/organizations_test.go @@ -53,9 +53,11 @@ type OrgTestSuite struct { adminCtx context.Context adminUserID string - testCreds params.GithubCredentials - secondaryTestCreds params.GithubCredentials - githubEndpoint params.GithubEndpoint + testCreds params.ForgeCredentials + testCredsGitea params.ForgeCredentials + secondaryTestCreds params.ForgeCredentials + githubEndpoint params.ForgeEndpoint + giteaEndpoint params.ForgeEndpoint } func (s *OrgTestSuite) equalInstancesByName(expected, actual []params.Instance) { @@ -91,7 +93,9 @@ func (s *OrgTestSuite) SetupTest() { s.Require().NotEmpty(s.adminUserID) s.githubEndpoint = garmTesting.CreateDefaultGithubEndpoint(adminCtx, db, s.T()) + s.giteaEndpoint = garmTesting.CreateDefaultGiteaEndpoint(adminCtx, db, s.T()) s.testCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), s.githubEndpoint) + s.testCredsGitea = garmTesting.CreateTestGiteaCredentials(adminCtx, "new-creds", db, s.T(), s.giteaEndpoint) s.secondaryTestCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "secondary-creds", db, s.T(), s.githubEndpoint) // create some organization objects in the database, for testing purposes @@ -100,7 +104,7 @@ func (s *OrgTestSuite) SetupTest() { org, err := db.CreateOrganization( s.adminCtx, fmt.Sprintf("test-org-%d", i), - s.testCreds.Name, + s.testCreds, fmt.Sprintf("test-webhook-secret-%d", i), params.PoolBalancerTypeRoundRobin, ) @@ -179,7 +183,7 @@ func (s *OrgTestSuite) TestCreateOrganization() { org, err := s.Store.CreateOrganization( s.adminCtx, s.Fixtures.CreateOrgParams.Name, - s.Fixtures.CreateOrgParams.CredentialsName, + s.testCreds, s.Fixtures.CreateOrgParams.WebhookSecret, params.PoolBalancerTypeRoundRobin) @@ -192,6 +196,62 @@ func (s *OrgTestSuite) TestCreateOrganization() { s.Require().Equal(storeOrg.Name, org.Name) s.Require().Equal(storeOrg.Credentials.Name, org.Credentials.Name) s.Require().Equal(storeOrg.WebhookSecret, org.WebhookSecret) + + entity, err := org.GetEntity() + s.Require().Nil(err) + s.Require().Equal(entity.EntityType, params.ForgeEntityTypeOrganization) + s.Require().Equal(entity.ID, org.ID) + + forgeType, err := entity.GetForgeType() + s.Require().Nil(err) + s.Require().Equal(forgeType, params.GithubEndpointType) +} + +func (s *OrgTestSuite) TestCreateOrgForGitea() { + // call tested function + org, err := s.Store.CreateOrganization( + s.adminCtx, + s.Fixtures.CreateOrgParams.Name, + s.testCredsGitea, + s.Fixtures.CreateOrgParams.WebhookSecret, + params.PoolBalancerTypeRoundRobin) + + // assertions + s.Require().Nil(err) + storeOrg, err := s.Store.GetOrganizationByID(s.adminCtx, org.ID) + if err != nil { + s.FailNow(fmt.Sprintf("failed to get organization by id: %v", err)) + } + s.Require().Equal(storeOrg.Name, org.Name) + s.Require().Equal(storeOrg.Credentials.Name, org.Credentials.Name) + s.Require().Equal(storeOrg.WebhookSecret, org.WebhookSecret) + + entity, err := org.GetEntity() + s.Require().Nil(err) + s.Require().Equal(entity.EntityType, params.ForgeEntityTypeOrganization) + s.Require().Equal(entity.ID, org.ID) + + forgeType, err := entity.GetForgeType() + s.Require().Nil(err) + s.Require().Equal(forgeType, params.GiteaEndpointType) +} + +func (s *OrgTestSuite) TestCreateOrganizationInvalidForgeType() { + credentials := params.ForgeCredentials{ + Name: "test-creds", + Endpoint: s.githubEndpoint, + ID: 99, + ForgeType: params.EndpointType("invalid-forge-type"), + } + + _, err := s.Store.CreateOrganization( + s.adminCtx, + s.Fixtures.CreateOrgParams.Name, + credentials, + s.Fixtures.CreateOrgParams.WebhookSecret, + params.PoolBalancerTypeRoundRobin) + s.Require().NotNil(err) + s.Require().Equal("creating org: unsupported credentials type: invalid request", err.Error()) } func (s *OrgTestSuite) TestCreateOrganizationInvalidDBPassphrase() { @@ -210,7 +270,7 @@ func (s *OrgTestSuite) TestCreateOrganizationInvalidDBPassphrase() { _, err = sqlDB.CreateOrganization( s.adminCtx, s.Fixtures.CreateOrgParams.Name, - s.Fixtures.CreateOrgParams.CredentialsName, + s.testCreds, s.Fixtures.CreateOrgParams.WebhookSecret, params.PoolBalancerTypeRoundRobin) @@ -220,15 +280,6 @@ func (s *OrgTestSuite) TestCreateOrganizationInvalidDBPassphrase() { func (s *OrgTestSuite) TestCreateOrganizationDBCreateErr() { s.Fixtures.SQLMock.ExpectBegin() - s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE user_id = ? AND name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). - WithArgs(s.adminUserID, s.Fixtures.Orgs[0].CredentialsName, 1). - WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). - AddRow(s.testCreds.ID, s.githubEndpoint.Name)) - s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). - WithArgs(s.testCreds.Endpoint.Name). - WillReturnRows(sqlmock.NewRows([]string{"name"}). - AddRow(s.githubEndpoint.Name)) s.Fixtures.SQLMock. ExpectExec(regexp.QuoteMeta("INSERT INTO `organizations`")). WillReturnError(fmt.Errorf("creating org mock error")) @@ -237,7 +288,7 @@ func (s *OrgTestSuite) TestCreateOrganizationDBCreateErr() { _, err := s.StoreSQLMocked.CreateOrganization( s.adminCtx, s.Fixtures.CreateOrgParams.Name, - s.Fixtures.CreateOrgParams.CredentialsName, + s.testCreds, s.Fixtures.CreateOrgParams.WebhookSecret, params.PoolBalancerTypeRoundRobin) @@ -492,9 +543,9 @@ func (s *OrgTestSuite) TestCreateOrganizationPoolMissingTags() { } func (s *OrgTestSuite) TestCreateOrganizationPoolInvalidOrgID() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "dummy-org-id", - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, } _, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) @@ -640,9 +691,9 @@ func (s *OrgTestSuite) TestListOrgPools() { } func (s *OrgTestSuite) TestListOrgPoolsInvalidOrgID() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "dummy-org-id", - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, } _, err := s.Store.ListEntityPools(s.adminCtx, entity) @@ -665,9 +716,9 @@ func (s *OrgTestSuite) TestGetOrganizationPool() { } func (s *OrgTestSuite) TestGetOrganizationPoolInvalidOrgID() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "dummy-org-id", - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, } _, err := s.Store.GetEntityPool(s.adminCtx, entity, "dummy-pool-id") @@ -691,9 +742,9 @@ func (s *OrgTestSuite) TestDeleteOrganizationPool() { } func (s *OrgTestSuite) TestDeleteOrganizationPoolInvalidOrgID() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "dummy-org-id", - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, } err := s.Store.DeleteEntityPool(s.adminCtx, entity, "dummy-pool-id") @@ -748,9 +799,9 @@ func (s *OrgTestSuite) TestListOrgInstances() { } func (s *OrgTestSuite) TestListOrgInstancesInvalidOrgID() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "dummy-org-id", - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, } _, err := s.Store.ListEntityInstances(s.adminCtx, entity) @@ -776,9 +827,9 @@ func (s *OrgTestSuite) TestUpdateOrganizationPool() { } func (s *OrgTestSuite) TestUpdateOrganizationPoolInvalidOrgID() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "dummy-org-id", - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, } _, err := s.Store.UpdateEntityPool(s.adminCtx, entity, "dummy-pool-id", s.Fixtures.UpdatePoolParams) diff --git a/database/sql/pools.go b/database/sql/pools.go index 5cb6d1368..24476fe80 100644 --- a/database/sql/pools.go +++ b/database/sql/pools.go @@ -86,7 +86,7 @@ func (s *sqlDatabase) DeletePoolByID(_ context.Context, poolID string) (err erro return nil } -func (s *sqlDatabase) getEntityPool(tx *gorm.DB, entityType params.GithubEntityType, entityID, poolID string, preload ...string) (Pool, error) { +func (s *sqlDatabase) getEntityPool(tx *gorm.DB, entityType params.ForgeEntityType, entityID, poolID string, preload ...string) (Pool, error) { if entityID == "" { return Pool{}, errors.Wrap(runnerErrors.ErrBadRequest, "missing entity id") } @@ -99,13 +99,13 @@ func (s *sqlDatabase) getEntityPool(tx *gorm.DB, entityType params.GithubEntityT var fieldName string var entityField string switch entityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: fieldName = entityTypeRepoName entityField = repositoryFieldName - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: fieldName = entityTypeOrgName entityField = organizationFieldName - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: fieldName = entityTypeEnterpriseName entityField = enterpriseFieldName default: @@ -135,7 +135,7 @@ func (s *sqlDatabase) getEntityPool(tx *gorm.DB, entityType params.GithubEntityT return pool, nil } -func (s *sqlDatabase) listEntityPools(tx *gorm.DB, entityType params.GithubEntityType, entityID string, preload ...string) ([]Pool, error) { +func (s *sqlDatabase) listEntityPools(tx *gorm.DB, entityType params.ForgeEntityType, entityID string, preload ...string) ([]Pool, error) { if _, err := uuid.Parse(entityID); err != nil { return nil, errors.Wrap(runnerErrors.ErrBadRequest, "parsing id") } @@ -147,13 +147,13 @@ func (s *sqlDatabase) listEntityPools(tx *gorm.DB, entityType params.GithubEntit var preloadEntity string var fieldName string switch entityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: fieldName = entityTypeRepoName preloadEntity = "Repository" - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: fieldName = entityTypeOrgName preloadEntity = "Organization" - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: fieldName = entityTypeEnterpriseName preloadEntity = "Enterprise" default: @@ -184,7 +184,7 @@ func (s *sqlDatabase) listEntityPools(tx *gorm.DB, entityType params.GithubEntit return pools, nil } -func (s *sqlDatabase) findPoolByTags(id string, poolType params.GithubEntityType, tags []string) ([]params.Pool, error) { +func (s *sqlDatabase) findPoolByTags(id string, poolType params.ForgeEntityType, tags []string) ([]params.Pool, error) { if len(tags) == 0 { return nil, runnerErrors.NewBadRequestError("missing tags") } @@ -195,11 +195,11 @@ func (s *sqlDatabase) findPoolByTags(id string, poolType params.GithubEntityType var fieldName string switch poolType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: fieldName = entityTypeRepoName - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: fieldName = entityTypeOrgName - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: fieldName = entityTypeEnterpriseName default: return nil, fmt.Errorf("invalid poolType: %v", poolType) @@ -238,7 +238,7 @@ func (s *sqlDatabase) findPoolByTags(id string, poolType params.GithubEntityType return ret, nil } -func (s *sqlDatabase) FindPoolsMatchingAllTags(_ context.Context, entityType params.GithubEntityType, entityID string, tags []string) ([]params.Pool, error) { +func (s *sqlDatabase) FindPoolsMatchingAllTags(_ context.Context, entityType params.ForgeEntityType, entityID string, tags []string) ([]params.Pool, error) { if len(tags) == 0 { return nil, runnerErrors.NewBadRequestError("missing tags") } @@ -254,7 +254,7 @@ func (s *sqlDatabase) FindPoolsMatchingAllTags(_ context.Context, entityType par return pools, nil } -func (s *sqlDatabase) CreateEntityPool(_ context.Context, entity params.GithubEntity, param params.CreatePoolParams) (pool params.Pool, err error) { +func (s *sqlDatabase) CreateEntityPool(_ context.Context, entity params.ForgeEntity, param params.CreatePoolParams) (pool params.Pool, err error) { if len(param.Tags) == 0 { return params.Pool{}, runnerErrors.NewBadRequestError("no tags specified") } @@ -289,11 +289,11 @@ func (s *sqlDatabase) CreateEntityPool(_ context.Context, entity params.GithubEn } switch entity.EntityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: newPool.RepoID = &entityID - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: newPool.OrgID = &entityID - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: newPool.EnterpriseID = &entityID } err = s.conn.Transaction(func(tx *gorm.DB) error { @@ -334,7 +334,7 @@ func (s *sqlDatabase) CreateEntityPool(_ context.Context, entity params.GithubEn return s.sqlToCommonPool(dbPool) } -func (s *sqlDatabase) GetEntityPool(_ context.Context, entity params.GithubEntity, poolID string) (params.Pool, error) { +func (s *sqlDatabase) GetEntityPool(_ context.Context, entity params.ForgeEntity, poolID string) (params.Pool, error) { pool, err := s.getEntityPool(s.conn, entity.EntityType, entity.ID, poolID, "Tags", "Instances") if err != nil { return params.Pool{}, fmt.Errorf("fetching pool: %w", err) @@ -342,7 +342,7 @@ func (s *sqlDatabase) GetEntityPool(_ context.Context, entity params.GithubEntit return s.sqlToCommonPool(pool) } -func (s *sqlDatabase) DeleteEntityPool(_ context.Context, entity params.GithubEntity, poolID string) (err error) { +func (s *sqlDatabase) DeleteEntityPool(_ context.Context, entity params.ForgeEntity, poolID string) (err error) { entityID, err := uuid.Parse(entity.ID) if err != nil { return errors.Wrap(runnerErrors.ErrBadRequest, "parsing id") @@ -363,11 +363,11 @@ func (s *sqlDatabase) DeleteEntityPool(_ context.Context, entity params.GithubEn } var fieldName string switch entity.EntityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: fieldName = entityTypeRepoName - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: fieldName = entityTypeOrgName - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: fieldName = entityTypeEnterpriseName default: return fmt.Errorf("invalid entityType: %v", entity.EntityType) @@ -379,7 +379,7 @@ func (s *sqlDatabase) DeleteEntityPool(_ context.Context, entity params.GithubEn return nil } -func (s *sqlDatabase) UpdateEntityPool(_ context.Context, entity params.GithubEntity, poolID string, param params.UpdatePoolParams) (updatedPool params.Pool, err error) { +func (s *sqlDatabase) UpdateEntityPool(_ context.Context, entity params.ForgeEntity, poolID string, param params.UpdatePoolParams) (updatedPool params.Pool, err error) { defer func() { if err == nil { s.sendNotify(common.PoolEntityType, common.UpdateOperation, updatedPool) @@ -403,7 +403,7 @@ func (s *sqlDatabase) UpdateEntityPool(_ context.Context, entity params.GithubEn return updatedPool, nil } -func (s *sqlDatabase) ListEntityPools(_ context.Context, entity params.GithubEntity) ([]params.Pool, error) { +func (s *sqlDatabase) ListEntityPools(_ context.Context, entity params.ForgeEntity) ([]params.Pool, error) { pools, err := s.listEntityPools(s.conn, entity.EntityType, entity.ID, "Tags") if err != nil { return nil, errors.Wrap(err, "fetching pools") @@ -420,7 +420,7 @@ func (s *sqlDatabase) ListEntityPools(_ context.Context, entity params.GithubEnt return ret, nil } -func (s *sqlDatabase) ListEntityInstances(_ context.Context, entity params.GithubEntity) ([]params.Instance, error) { +func (s *sqlDatabase) ListEntityInstances(_ context.Context, entity params.ForgeEntity) ([]params.Instance, error) { pools, err := s.listEntityPools(s.conn, entity.EntityType, entity.ID, "Instances", "Instances.Job") if err != nil { return nil, errors.Wrap(err, "fetching entity") diff --git a/database/sql/pools_test.go b/database/sql/pools_test.go index 758dcacdf..9044bf18f 100644 --- a/database/sql/pools_test.go +++ b/database/sql/pools_test.go @@ -81,7 +81,7 @@ func (s *PoolsTestSuite) SetupTest() { creds := garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), githubEndpoint) // create an organization for testing purposes - org, err := s.Store.CreateOrganization(s.adminCtx, "test-org", creds.Name, "test-webhookSecret", params.PoolBalancerTypeRoundRobin) + org, err := s.Store.CreateOrganization(s.adminCtx, "test-org", creds, "test-webhookSecret", params.PoolBalancerTypeRoundRobin) if err != nil { s.FailNow(fmt.Sprintf("failed to create org: %s", err)) } @@ -211,7 +211,7 @@ func (s *PoolsTestSuite) TestEntityPoolOperations() { ep := garmTesting.CreateDefaultGithubEndpoint(s.ctx, s.Store, s.T()) creds := garmTesting.CreateTestGithubCredentials(s.ctx, "test-creds", s.Store, s.T(), ep) s.T().Cleanup(func() { s.Store.DeleteGithubCredentials(s.ctx, creds.ID) }) - repo, err := s.Store.CreateRepository(s.ctx, "test-owner", "test-repo", creds.Name, "test-secret", params.PoolBalancerTypeRoundRobin) + repo, err := s.Store.CreateRepository(s.ctx, "test-owner", "test-repo", creds, "test-secret", params.PoolBalancerTypeRoundRobin) s.Require().NoError(err) s.Require().NotEmpty(repo.ID) s.T().Cleanup(func() { s.Store.DeleteRepository(s.ctx, repo.ID) }) @@ -291,7 +291,7 @@ func (s *PoolsTestSuite) TestListEntityInstances() { ep := garmTesting.CreateDefaultGithubEndpoint(s.ctx, s.Store, s.T()) creds := garmTesting.CreateTestGithubCredentials(s.ctx, "test-creds", s.Store, s.T(), ep) s.T().Cleanup(func() { s.Store.DeleteGithubCredentials(s.ctx, creds.ID) }) - repo, err := s.Store.CreateRepository(s.ctx, "test-owner", "test-repo", creds.Name, "test-secret", params.PoolBalancerTypeRoundRobin) + repo, err := s.Store.CreateRepository(s.ctx, "test-owner", "test-repo", creds, "test-secret", params.PoolBalancerTypeRoundRobin) s.Require().NoError(err) s.Require().NotEmpty(repo.ID) s.T().Cleanup(func() { s.Store.DeleteRepository(s.ctx, repo.ID) }) diff --git a/database/sql/repositories.go b/database/sql/repositories.go index 6b744163a..d74190707 100644 --- a/database/sql/repositories.go +++ b/database/sql/repositories.go @@ -29,7 +29,7 @@ import ( "github.com/cloudbase/garm/params" ) -func (s *sqlDatabase) CreateRepository(ctx context.Context, owner, name, credentialsName, webhookSecret string, poolBalancerType params.PoolBalancerType) (param params.Repository, err error) { +func (s *sqlDatabase) CreateRepository(ctx context.Context, owner, name string, credentials params.ForgeCredentials, webhookSecret string, poolBalancerType params.PoolBalancerType) (param params.Repository, err error) { defer func() { if err == nil { s.sendNotify(common.RepositoryEntityType, common.CreateOperation, param) @@ -51,32 +51,32 @@ func (s *sqlDatabase) CreateRepository(ctx context.Context, owner, name, credent PoolBalancerType: poolBalancerType, } err = s.conn.Transaction(func(tx *gorm.DB) error { - creds, err := s.getGithubCredentialsByName(ctx, tx, credentialsName, false) - if err != nil { - return errors.Wrap(err, "creating repository") - } - if creds.EndpointName == nil { - return errors.Wrap(runnerErrors.ErrUnprocessable, "credentials have no endpoint") + switch credentials.ForgeType { + case params.GithubEndpointType: + newRepo.CredentialsID = &credentials.ID + case params.GiteaEndpointType: + newRepo.GiteaCredentialsID = &credentials.ID + default: + return errors.Wrap(runnerErrors.ErrBadRequest, "unsupported credentials type") } - newRepo.CredentialsID = &creds.ID - newRepo.CredentialsName = creds.Name - newRepo.EndpointName = creds.EndpointName + newRepo.EndpointName = &credentials.Endpoint.Name q := tx.Create(&newRepo) if q.Error != nil { return errors.Wrap(q.Error, "creating repository") } - - newRepo.Credentials = creds - newRepo.Endpoint = creds.Endpoint - return nil }) if err != nil { return params.Repository{}, errors.Wrap(err, "creating repository") } - param, err = s.sqlToCommonRepository(newRepo, true) + repo, err := s.getRepoByID(ctx, s.conn, newRepo.ID.String(), "Endpoint", "Credentials", "GiteaCredentials", "Credentials.Endpoint", "GiteaCredentials.Endpoint") + if err != nil { + return params.Repository{}, errors.Wrap(err, "creating repository") + } + + param, err = s.sqlToCommonRepository(repo, true) if err != nil { return params.Repository{}, errors.Wrap(err, "creating repository") } @@ -102,7 +102,9 @@ func (s *sqlDatabase) ListRepositories(_ context.Context) ([]params.Repository, var repos []Repository q := s.conn. Preload("Credentials"). + Preload("GiteaCredentials"). Preload("Credentials.Endpoint"). + Preload("GiteaCredentials.Endpoint"). Preload("Endpoint"). Find(&repos) if q.Error != nil { @@ -122,7 +124,7 @@ func (s *sqlDatabase) ListRepositories(_ context.Context) ([]params.Repository, } func (s *sqlDatabase) DeleteRepository(ctx context.Context, repoID string) (err error) { - repo, err := s.getRepoByID(ctx, s.conn, repoID, "Endpoint", "Credentials", "Credentials.Endpoint") + repo, err := s.getRepoByID(ctx, s.conn, repoID, "Endpoint", "Credentials", "Credentials.Endpoint", "GiteaCredentials", "GiteaCredentials.Endpoint") if err != nil { return errors.Wrap(err, "fetching repo") } @@ -165,7 +167,6 @@ func (s *sqlDatabase) UpdateRepository(ctx context.Context, repoID string, param } if param.CredentialsName != "" { - repo.CredentialsName = param.CredentialsName creds, err = s.getGithubCredentialsByName(ctx, tx, param.CredentialsName, false) if err != nil { return errors.Wrap(err, "fetching credentials") @@ -203,7 +204,7 @@ func (s *sqlDatabase) UpdateRepository(ctx context.Context, repoID string, param return params.Repository{}, errors.Wrap(err, "saving repo") } - repo, err = s.getRepoByID(ctx, s.conn, repoID, "Endpoint", "Credentials", "Credentials.Endpoint") + repo, err = s.getRepoByID(ctx, s.conn, repoID, "Endpoint", "Credentials", "Credentials.Endpoint", "GiteaCredentials", "GiteaCredentials.Endpoint") if err != nil { return params.Repository{}, errors.Wrap(err, "updating enterprise") } @@ -216,7 +217,7 @@ func (s *sqlDatabase) UpdateRepository(ctx context.Context, repoID string, param } func (s *sqlDatabase) GetRepositoryByID(ctx context.Context, repoID string) (params.Repository, error) { - repo, err := s.getRepoByID(ctx, s.conn, repoID, "Pools", "Credentials", "Endpoint", "Credentials.Endpoint") + repo, err := s.getRepoByID(ctx, s.conn, repoID, "Pools", "Credentials", "Endpoint", "Credentials.Endpoint", "GiteaCredentials", "GiteaCredentials.Endpoint") if err != nil { return params.Repository{}, errors.Wrap(err, "fetching repo") } @@ -234,6 +235,8 @@ func (s *sqlDatabase) getRepo(_ context.Context, owner, name, endpointName strin q := s.conn.Where("name = ? COLLATE NOCASE and owner = ? COLLATE NOCASE and endpoint_name = ? COLLATE NOCASE", name, owner, endpointName). Preload("Credentials"). Preload("Credentials.Endpoint"). + Preload("GiteaCredentials"). + Preload("GiteaCredentials.Endpoint"). Preload("Endpoint"). First(&repo) diff --git a/database/sql/repositories_test.go b/database/sql/repositories_test.go index 484742aea..f27e10b5b 100644 --- a/database/sql/repositories_test.go +++ b/database/sql/repositories_test.go @@ -58,9 +58,11 @@ type RepoTestSuite struct { adminCtx context.Context adminUserID string - testCreds params.GithubCredentials - secondaryTestCreds params.GithubCredentials - githubEndpoint params.GithubEndpoint + testCreds params.ForgeCredentials + testCredsGitea params.ForgeCredentials + secondaryTestCreds params.ForgeCredentials + githubEndpoint params.ForgeEndpoint + giteaEndpoint params.ForgeEndpoint } func (s *RepoTestSuite) equalReposByName(expected, actual []params.Repository) { @@ -109,7 +111,9 @@ func (s *RepoTestSuite) SetupTest() { s.Require().NotEmpty(s.adminUserID) s.githubEndpoint = garmTesting.CreateDefaultGithubEndpoint(adminCtx, db, s.T()) + s.giteaEndpoint = garmTesting.CreateDefaultGiteaEndpoint(adminCtx, db, s.T()) s.testCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), s.githubEndpoint) + s.testCredsGitea = garmTesting.CreateTestGiteaCredentials(adminCtx, "new-creds", db, s.T(), s.giteaEndpoint) s.secondaryTestCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "secondary-creds", db, s.T(), s.githubEndpoint) // create some repository objects in the database, for testing purposes @@ -119,7 +123,7 @@ func (s *RepoTestSuite) SetupTest() { adminCtx, fmt.Sprintf("test-owner-%d", i), fmt.Sprintf("test-repo-%d", i), - s.testCreds.Name, + s.testCreds, fmt.Sprintf("test-webhook-secret-%d", i), params.PoolBalancerTypeRoundRobin, ) @@ -204,7 +208,7 @@ func (s *RepoTestSuite) TestCreateRepository() { s.adminCtx, s.Fixtures.CreateRepoParams.Owner, s.Fixtures.CreateRepoParams.Name, - s.Fixtures.CreateRepoParams.CredentialsName, + s.testCreds, s.Fixtures.CreateRepoParams.WebhookSecret, params.PoolBalancerTypeRoundRobin, ) @@ -219,6 +223,68 @@ func (s *RepoTestSuite) TestCreateRepository() { s.Require().Equal(storeRepo.Name, repo.Name) s.Require().Equal(storeRepo.Credentials.Name, repo.Credentials.Name) s.Require().Equal(storeRepo.WebhookSecret, repo.WebhookSecret) + + entity, err := repo.GetEntity() + s.Require().Nil(err) + s.Require().Equal(s.Fixtures.CreateRepoParams.Owner, entity.Owner) + s.Require().Equal(entity.EntityType, params.ForgeEntityTypeRepository) + + forgeType, err := entity.GetForgeType() + s.Require().Nil(err) + s.Require().Equal(forgeType, params.GithubEndpointType) +} + +func (s *RepoTestSuite) TestCreateRepositoryGitea() { + // call tested function + repo, err := s.Store.CreateRepository( + s.adminCtx, + s.Fixtures.CreateRepoParams.Owner, + s.Fixtures.CreateRepoParams.Name, + s.testCredsGitea, + s.Fixtures.CreateRepoParams.WebhookSecret, + params.PoolBalancerTypeRoundRobin, + ) + + // assertions + s.Require().Nil(err) + storeRepo, err := s.Store.GetRepositoryByID(s.adminCtx, repo.ID) + if err != nil { + s.FailNow(fmt.Sprintf("failed to get repository by id: %v", err)) + } + s.Require().Equal(storeRepo.Owner, repo.Owner) + s.Require().Equal(storeRepo.Name, repo.Name) + s.Require().Equal(storeRepo.Credentials.Name, repo.Credentials.Name) + s.Require().Equal(storeRepo.WebhookSecret, repo.WebhookSecret) + + entity, err := repo.GetEntity() + s.Require().Nil(err) + s.Require().Equal(repo.ID, entity.ID) + s.Require().Equal(entity.EntityType, params.ForgeEntityTypeRepository) + + forgeType, err := entity.GetForgeType() + s.Require().Nil(err) + s.Require().Equal(forgeType, params.GiteaEndpointType) +} + +func (s *RepoTestSuite) TestCreateRepositoryInvalidForgeType() { + // call tested function + _, err := s.Store.CreateRepository( + s.adminCtx, + s.Fixtures.CreateRepoParams.Owner, + s.Fixtures.CreateRepoParams.Name, + params.ForgeCredentials{ + Name: "test-creds", + ForgeType: "invalid-forge-type", + Endpoint: params.ForgeEndpoint{ + Name: "test-endpoint", + }, + }, + s.Fixtures.CreateRepoParams.WebhookSecret, + params.PoolBalancerTypeRoundRobin, + ) + + s.Require().NotNil(err) + s.Require().Equal("creating repository: unsupported credentials type: invalid request", err.Error()) } func (s *RepoTestSuite) TestCreateRepositoryInvalidDBPassphrase() { @@ -238,7 +304,7 @@ func (s *RepoTestSuite) TestCreateRepositoryInvalidDBPassphrase() { s.adminCtx, s.Fixtures.CreateRepoParams.Owner, s.Fixtures.CreateRepoParams.Name, - s.Fixtures.CreateRepoParams.CredentialsName, + s.testCreds, s.Fixtures.CreateRepoParams.WebhookSecret, params.PoolBalancerTypeRoundRobin, ) @@ -249,15 +315,6 @@ func (s *RepoTestSuite) TestCreateRepositoryInvalidDBPassphrase() { func (s *RepoTestSuite) TestCreateRepositoryInvalidDBCreateErr() { s.Fixtures.SQLMock.ExpectBegin() - s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE user_id = ? AND name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). - WithArgs(s.adminUserID, s.Fixtures.Repos[0].CredentialsName, 1). - WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). - AddRow(s.testCreds.ID, s.githubEndpoint.Name)) - s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). - WithArgs(s.testCreds.Endpoint.Name). - WillReturnRows(sqlmock.NewRows([]string{"name"}). - AddRow(s.githubEndpoint.Name)) s.Fixtures.SQLMock. ExpectExec(regexp.QuoteMeta("INSERT INTO `repositories`")). WillReturnError(fmt.Errorf("creating repo mock error")) @@ -267,7 +324,7 @@ func (s *RepoTestSuite) TestCreateRepositoryInvalidDBCreateErr() { s.adminCtx, s.Fixtures.CreateRepoParams.Owner, s.Fixtures.CreateRepoParams.Name, - s.Fixtures.CreateRepoParams.CredentialsName, + s.testCreds, s.Fixtures.CreateRepoParams.WebhookSecret, params.PoolBalancerTypeRoundRobin, ) @@ -541,9 +598,9 @@ func (s *RepoTestSuite) TestCreateRepositoryPoolMissingTags() { } func (s *RepoTestSuite) TestCreateRepositoryPoolInvalidRepoID() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "dummy-repo-id", - EntityType: params.GithubEntityTypeRepository, + EntityType: params.ForgeEntityTypeRepository, } _, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) @@ -692,9 +749,9 @@ func (s *RepoTestSuite) TestListRepoPools() { } func (s *RepoTestSuite) TestListRepoPoolsInvalidRepoID() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "dummy-repo-id", - EntityType: params.GithubEntityTypeRepository, + EntityType: params.ForgeEntityTypeRepository, } _, err := s.Store.ListEntityPools(s.adminCtx, entity) @@ -717,9 +774,9 @@ func (s *RepoTestSuite) TestGetRepositoryPool() { } func (s *RepoTestSuite) TestGetRepositoryPoolInvalidRepoID() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "dummy-repo-id", - EntityType: params.GithubEntityTypeRepository, + EntityType: params.ForgeEntityTypeRepository, } _, err := s.Store.GetEntityPool(s.adminCtx, entity, "dummy-pool-id") @@ -743,9 +800,9 @@ func (s *RepoTestSuite) TestDeleteRepositoryPool() { } func (s *RepoTestSuite) TestDeleteRepositoryPoolInvalidRepoID() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "dummy-repo-id", - EntityType: params.GithubEntityTypeRepository, + EntityType: params.ForgeEntityTypeRepository, } err := s.Store.DeleteEntityPool(s.adminCtx, entity, "dummy-pool-id") @@ -799,9 +856,9 @@ func (s *RepoTestSuite) TestListRepoInstances() { } func (s *RepoTestSuite) TestListRepoInstancesInvalidRepoID() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "dummy-repo-id", - EntityType: params.GithubEntityTypeRepository, + EntityType: params.ForgeEntityTypeRepository, } _, err := s.Store.ListEntityInstances(s.adminCtx, entity) @@ -827,9 +884,9 @@ func (s *RepoTestSuite) TestUpdateRepositoryPool() { } func (s *RepoTestSuite) TestUpdateRepositoryPoolInvalidRepoID() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: "dummy-repo-id", - EntityType: params.GithubEntityTypeRepository, + EntityType: params.ForgeEntityTypeRepository, } _, err := s.Store.UpdateEntityPool(s.adminCtx, entity, "dummy-repo-id", s.Fixtures.UpdatePoolParams) diff --git a/database/sql/scaleset_instances.go b/database/sql/scaleset_instances.go index fcb9e1f29..bbc4f593d 100644 --- a/database/sql/scaleset_instances.go +++ b/database/sql/scaleset_instances.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package sql import ( diff --git a/database/sql/scalesets.go b/database/sql/scalesets.go index e4bd28f34..930ae17d8 100644 --- a/database/sql/scalesets.go +++ b/database/sql/scalesets.go @@ -1,16 +1,16 @@ -// Copyright 2024 Cloudbase Solutions SRL +// Copyright 2025 Cloudbase Solutions SRL // -// Licensed under the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package sql @@ -53,7 +53,7 @@ func (s *sqlDatabase) ListAllScaleSets(_ context.Context) ([]params.ScaleSet, er return ret, nil } -func (s *sqlDatabase) CreateEntityScaleSet(_ context.Context, entity params.GithubEntity, param params.CreateScaleSetParams) (scaleSet params.ScaleSet, err error) { +func (s *sqlDatabase) CreateEntityScaleSet(_ context.Context, entity params.ForgeEntity, param params.CreateScaleSetParams) (scaleSet params.ScaleSet, err error) { if err := param.Validate(); err != nil { return params.ScaleSet{}, fmt.Errorf("failed to validate create params: %w", err) } @@ -92,11 +92,11 @@ func (s *sqlDatabase) CreateEntityScaleSet(_ context.Context, entity params.Gith } switch entity.EntityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: newScaleSet.RepoID = &entityID - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: newScaleSet.OrgID = &entityID - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: newScaleSet.EnterpriseID = &entityID } err = s.conn.Transaction(func(tx *gorm.DB) error { @@ -123,7 +123,7 @@ func (s *sqlDatabase) CreateEntityScaleSet(_ context.Context, entity params.Gith return s.sqlToCommonScaleSet(dbScaleSet) } -func (s *sqlDatabase) listEntityScaleSets(tx *gorm.DB, entityType params.GithubEntityType, entityID string, preload ...string) ([]ScaleSet, error) { +func (s *sqlDatabase) listEntityScaleSets(tx *gorm.DB, entityType params.ForgeEntityType, entityID string, preload ...string) ([]ScaleSet, error) { if _, err := uuid.Parse(entityID); err != nil { return nil, errors.Wrap(runnerErrors.ErrBadRequest, "parsing id") } @@ -135,13 +135,13 @@ func (s *sqlDatabase) listEntityScaleSets(tx *gorm.DB, entityType params.GithubE var preloadEntity string var fieldName string switch entityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: fieldName = entityTypeRepoName preloadEntity = repositoryFieldName - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: fieldName = entityTypeOrgName preloadEntity = organizationFieldName - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: fieldName = entityTypeEnterpriseName preloadEntity = enterpriseFieldName default: @@ -173,7 +173,7 @@ func (s *sqlDatabase) listEntityScaleSets(tx *gorm.DB, entityType params.GithubE return scaleSets, nil } -func (s *sqlDatabase) ListEntityScaleSets(_ context.Context, entity params.GithubEntity) ([]params.ScaleSet, error) { +func (s *sqlDatabase) ListEntityScaleSets(_ context.Context, entity params.ForgeEntity) ([]params.ScaleSet, error) { scaleSets, err := s.listEntityScaleSets(s.conn, entity.EntityType, entity.ID) if err != nil { return nil, errors.Wrap(err, "fetching scale sets") @@ -190,7 +190,7 @@ func (s *sqlDatabase) ListEntityScaleSets(_ context.Context, entity params.Githu return ret, nil } -func (s *sqlDatabase) UpdateEntityScaleSet(_ context.Context, entity params.GithubEntity, scaleSetID uint, param params.UpdateScaleSetParams, callback func(old, newSet params.ScaleSet) error) (updatedScaleSet params.ScaleSet, err error) { +func (s *sqlDatabase) UpdateEntityScaleSet(_ context.Context, entity params.ForgeEntity, scaleSetID uint, param params.UpdateScaleSetParams, callback func(old, newSet params.ScaleSet) error) (updatedScaleSet params.ScaleSet, err error) { defer func() { if err == nil { s.sendNotify(common.ScaleSetEntityType, common.UpdateOperation, updatedScaleSet) @@ -225,7 +225,7 @@ func (s *sqlDatabase) UpdateEntityScaleSet(_ context.Context, entity params.Gith return updatedScaleSet, nil } -func (s *sqlDatabase) getEntityScaleSet(tx *gorm.DB, entityType params.GithubEntityType, entityID string, scaleSetID uint, preload ...string) (ScaleSet, error) { +func (s *sqlDatabase) getEntityScaleSet(tx *gorm.DB, entityType params.ForgeEntityType, entityID string, scaleSetID uint, preload ...string) (ScaleSet, error) { if entityID == "" { return ScaleSet{}, errors.Wrap(runnerErrors.ErrBadRequest, "missing entity id") } @@ -237,13 +237,13 @@ func (s *sqlDatabase) getEntityScaleSet(tx *gorm.DB, entityType params.GithubEnt var fieldName string var entityField string switch entityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: fieldName = entityTypeRepoName entityField = "Repository" - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: fieldName = entityTypeOrgName entityField = "Organization" - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: fieldName = entityTypeEnterpriseName entityField = "Enterprise" default: diff --git a/database/sql/scalesets_test.go b/database/sql/scalesets_test.go index 951c37350..f1f9fbbaf 100644 --- a/database/sql/scalesets_test.go +++ b/database/sql/scalesets_test.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package sql import ( @@ -19,15 +33,15 @@ type ScaleSetsTestSuite struct { suite.Suite Store dbCommon.Store adminCtx context.Context - creds params.GithubCredentials + creds params.ForgeCredentials org params.Organization repo params.Repository enterprise params.Enterprise - orgEntity params.GithubEntity - repoEntity params.GithubEntity - enterpriseEntity params.GithubEntity + orgEntity params.ForgeEntity + repoEntity params.ForgeEntity + enterpriseEntity params.ForgeEntity } func (s *ScaleSetsTestSuite) SetupTest() { @@ -48,17 +62,17 @@ func (s *ScaleSetsTestSuite) SetupTest() { s.creds = garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), githubEndpoint) // create an organization for testing purposes - s.org, err = s.Store.CreateOrganization(s.adminCtx, "test-org", s.creds.Name, "test-webhookSecret", params.PoolBalancerTypeRoundRobin) + s.org, err = s.Store.CreateOrganization(s.adminCtx, "test-org", s.creds, "test-webhookSecret", params.PoolBalancerTypeRoundRobin) if err != nil { s.FailNow(fmt.Sprintf("failed to create org: %s", err)) } - s.repo, err = s.Store.CreateRepository(s.adminCtx, "test-org", "test-repo", s.creds.Name, "test-webhookSecret", params.PoolBalancerTypeRoundRobin) + s.repo, err = s.Store.CreateRepository(s.adminCtx, "test-org", "test-repo", s.creds, "test-webhookSecret", params.PoolBalancerTypeRoundRobin) if err != nil { s.FailNow(fmt.Sprintf("failed to create repo: %s", err)) } - s.enterprise, err = s.Store.CreateEnterprise(s.adminCtx, "test-enterprise", s.creds.Name, "test-webhookSecret", params.PoolBalancerTypeRoundRobin) + s.enterprise, err = s.Store.CreateEnterprise(s.adminCtx, "test-enterprise", s.creds, "test-webhookSecret", params.PoolBalancerTypeRoundRobin) if err != nil { s.FailNow(fmt.Sprintf("failed to create enterprise: %s", err)) } @@ -298,7 +312,7 @@ func (s *ScaleSetsTestSuite) TestScaleSetOperations() { }) s.T().Run("update scaleset with invalid entity", func(_ *testing.T) { - _, err = s.Store.UpdateEntityScaleSet(s.adminCtx, params.GithubEntity{}, enterpriseScaleSet.ID, params.UpdateScaleSetParams{}, nil) + _, err = s.Store.UpdateEntityScaleSet(s.adminCtx, params.ForgeEntity{}, enterpriseScaleSet.ID, params.UpdateScaleSetParams{}, nil) s.Require().Error(err) s.Require().Contains(err.Error(), "missing entity id") }) diff --git a/database/sql/sql.go b/database/sql/sql.go index 76495732c..167e90edc 100644 --- a/database/sql/sql.go +++ b/database/sql/sql.go @@ -299,7 +299,7 @@ func (s *sqlDatabase) migrateCredentialsToDB() (err error) { CACertBundle: certBundle, } - var endpoint params.GithubEndpoint + var endpoint params.ForgeEndpoint endpoint, err = s.GetGithubEndpoint(adminCtx, hostname) if err != nil { if !errors.Is(err, runnerErrors.ErrNotFound) { @@ -315,10 +315,10 @@ func (s *sqlDatabase) migrateCredentialsToDB() (err error) { Name: cred.Name, Description: cred.Description, Endpoint: endpoint.Name, - AuthType: params.GithubAuthType(cred.GetAuthType()), + AuthType: params.ForgeAuthType(cred.GetAuthType()), } switch credParams.AuthType { - case params.GithubAuthTypeApp: + case params.ForgeAuthTypeApp: keyBytes, err := cred.App.PrivateKeyBytes() if err != nil { return errors.Wrap(err, "getting private key bytes") @@ -332,7 +332,7 @@ func (s *sqlDatabase) migrateCredentialsToDB() (err error) { if err := credParams.App.Validate(); err != nil { return errors.Wrap(err, "validating app credentials") } - case params.GithubAuthTypePAT: + case params.ForgeAuthTypePAT: token := cred.PAT.OAuth2Token if token == "" { token = cred.OAuth2Token @@ -409,6 +409,17 @@ func (s *sqlDatabase) migrateDB() error { } } + if s.conn.Migrator().HasTable(&GithubEndpoint{}) { + if !s.conn.Migrator().HasColumn(&GithubEndpoint{}, "endpoint_type") { + if err := s.conn.Migrator().AutoMigrate(&GithubEndpoint{}); err != nil { + return errors.Wrap(err, "migrating github endpoints") + } + if err := s.conn.Exec("update github_endpoints set endpoint_type = 'github' where endpoint_type is null").Error; err != nil { + return errors.Wrap(err, "updating github endpoints") + } + } + } + var needsCredentialMigration bool if !s.conn.Migrator().HasTable(&GithubCredentials{}) || !s.conn.Migrator().HasTable(&GithubEndpoint{}) { needsCredentialMigration = true @@ -424,6 +435,7 @@ func (s *sqlDatabase) migrateDB() error { &User{}, &GithubEndpoint{}, &GithubCredentials{}, + &GiteaCredentials{}, &Tag{}, &Pool{}, &Repository{}, diff --git a/database/sql/util.go b/database/sql/util.go index 62f221792..11d338ba8 100644 --- a/database/sql/util.go +++ b/database/sql/util.go @@ -27,6 +27,7 @@ import ( runnerErrors "github.com/cloudbase/garm-provider-common/errors" commonParams "github.com/cloudbase/garm-provider-common/params" "github.com/cloudbase/garm-provider-common/util" + "github.com/cloudbase/garm/auth" dbCommon "github.com/cloudbase/garm/database/common" "github.com/cloudbase/garm/params" ) @@ -150,16 +151,24 @@ func (s *sqlDatabase) sqlToCommonOrganization(org Organization, detailed bool) ( UpdatedAt: org.UpdatedAt, } + var forgeCreds params.ForgeCredentials if org.CredentialsID != nil { ret.CredentialsID = *org.CredentialsID + forgeCreds, err = s.sqlToCommonForgeCredentials(org.Credentials) + } + + if org.GiteaCredentialsID != nil { + ret.CredentialsID = *org.GiteaCredentialsID + forgeCreds, err = s.sqlGiteaToCommonForgeCredentials(org.GiteaCredentials) + } + + if err != nil { + return params.Organization{}, errors.Wrap(err, "converting credentials") } if detailed { - creds, err := s.sqlToCommonGithubCredentials(org.Credentials) - if err != nil { - return params.Organization{}, errors.Wrap(err, "converting credentials") - } - ret.Credentials = creds + ret.Credentials = forgeCreds + ret.CredentialsName = forgeCreds.Name } if ret.PoolBalancerType == "" { @@ -206,7 +215,7 @@ func (s *sqlDatabase) sqlToCommonEnterprise(enterprise Enterprise, detailed bool } if detailed { - creds, err := s.sqlToCommonGithubCredentials(enterprise.Credentials) + creds, err := s.sqlToCommonForgeCredentials(enterprise.Credentials) if err != nil { return params.Enterprise{}, errors.Wrap(err, "converting credentials") } @@ -371,16 +380,28 @@ func (s *sqlDatabase) sqlToCommonRepository(repo Repository, detailed bool) (par Endpoint: endpoint, } + if repo.CredentialsID != nil && repo.GiteaCredentialsID != nil { + return params.Repository{}, runnerErrors.NewConflictError("both gitea and github credentials are set for repo %s", repo.Name) + } + + var forgeCreds params.ForgeCredentials if repo.CredentialsID != nil { ret.CredentialsID = *repo.CredentialsID + forgeCreds, err = s.sqlToCommonForgeCredentials(repo.Credentials) + } + + if repo.GiteaCredentialsID != nil { + ret.CredentialsID = *repo.GiteaCredentialsID + forgeCreds, err = s.sqlGiteaToCommonForgeCredentials(repo.GiteaCredentials) + } + + if err != nil { + return params.Repository{}, errors.Wrap(err, "converting credentials") } if detailed { - creds, err := s.sqlToCommonGithubCredentials(repo.Credentials) - if err != nil { - return params.Repository{}, errors.Wrap(err, "converting credentials") - } - ret.Credentials = creds + ret.Credentials = forgeCreds + ret.CredentialsName = forgeCreds.Name } if ret.PoolBalancerType == "" { @@ -546,18 +567,18 @@ func (s *sqlDatabase) getScaleSetByID(tx *gorm.DB, scaleSetID uint, preload ...s return scaleSet, nil } -func (s *sqlDatabase) hasGithubEntity(tx *gorm.DB, entityType params.GithubEntityType, entityID string) error { +func (s *sqlDatabase) hasGithubEntity(tx *gorm.DB, entityType params.ForgeEntityType, entityID string) error { u, err := uuid.Parse(entityID) if err != nil { return errors.Wrap(runnerErrors.ErrBadRequest, "parsing id") } var q *gorm.DB switch entityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: q = tx.Model(&Repository{}).Where("id = ?", u) - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: q = tx.Model(&Organization{}).Where("id = ?", u) - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: q = tx.Model(&Enterprise{}).Where("id = ?", u) default: return errors.Wrap(runnerErrors.ErrBadRequest, "invalid entity type") @@ -608,26 +629,26 @@ func (s *sqlDatabase) sendNotify(entityType dbCommon.DatabaseEntityType, op dbCo return s.producer.Notify(message) } -func (s *sqlDatabase) GetGithubEntity(_ context.Context, entityType params.GithubEntityType, entityID string) (params.GithubEntity, error) { +func (s *sqlDatabase) GetForgeEntity(_ context.Context, entityType params.ForgeEntityType, entityID string) (params.ForgeEntity, error) { var ghEntity params.EntityGetter var err error switch entityType { - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: ghEntity, err = s.GetEnterpriseByID(s.ctx, entityID) - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: ghEntity, err = s.GetOrganizationByID(s.ctx, entityID) - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: ghEntity, err = s.GetRepositoryByID(s.ctx, entityID) default: - return params.GithubEntity{}, errors.Wrap(runnerErrors.ErrBadRequest, "invalid entity type") + return params.ForgeEntity{}, errors.Wrap(runnerErrors.ErrBadRequest, "invalid entity type") } if err != nil { - return params.GithubEntity{}, errors.Wrap(err, "failed to get ") + return params.ForgeEntity{}, errors.Wrap(err, "failed to get ") } entity, err := ghEntity.GetEntity() if err != nil { - return params.GithubEntity{}, errors.Wrap(err, "failed to get entity") + return params.ForgeEntity{}, errors.Wrap(err, "failed to get entity") } return entity, nil } @@ -638,7 +659,7 @@ func (s *sqlDatabase) addRepositoryEvent(ctx context.Context, repoID string, eve return errors.Wrap(err, "updating instance") } - msg := InstanceStatusUpdate{ + msg := RepositoryEvent{ Message: statusMessage, EventType: event, EventLevel: eventLevel, @@ -653,8 +674,8 @@ func (s *sqlDatabase) addRepositoryEvent(ctx context.Context, repoID string, eve if err != nil { return errors.Wrap(runnerErrors.ErrBadRequest, "parsing id") } - var latestEvents []OrganizationEvent - q := s.conn.Model(&OrganizationEvent{}). + var latestEvents []RepositoryEvent + q := s.conn.Model(&RepositoryEvent{}). Limit(maxEvents).Order("id desc"). Where("repo_id = ?", repoID).Find(&latestEvents) if q.Error != nil { @@ -662,7 +683,7 @@ func (s *sqlDatabase) addRepositoryEvent(ctx context.Context, repoID string, eve } if len(latestEvents) == maxEvents { lastInList := latestEvents[len(latestEvents)-1] - if err := s.conn.Where("repo_id = ? and id < ?", repoID, lastInList.ID).Unscoped().Delete(&OrganizationEvent{}).Error; err != nil { + if err := s.conn.Where("repo_id = ? and id < ?", repoID, lastInList.ID).Unscoped().Delete(&RepositoryEvent{}).Error; err != nil { return errors.Wrap(err, "deleting old events") } } @@ -676,7 +697,7 @@ func (s *sqlDatabase) addOrgEvent(ctx context.Context, orgID string, event param return errors.Wrap(err, "updating instance") } - msg := InstanceStatusUpdate{ + msg := OrganizationEvent{ Message: statusMessage, EventType: event, EventLevel: eventLevel, @@ -714,7 +735,7 @@ func (s *sqlDatabase) addEnterpriseEvent(ctx context.Context, entID string, even return errors.Wrap(err, "updating instance") } - msg := InstanceStatusUpdate{ + msg := EnterpriseEvent{ Message: statusMessage, EventType: event, EventLevel: eventLevel, @@ -747,19 +768,151 @@ func (s *sqlDatabase) addEnterpriseEvent(ctx context.Context, entID string, even return nil } -func (s *sqlDatabase) AddEntityEvent(ctx context.Context, entity params.GithubEntity, event params.EventType, eventLevel params.EventLevel, statusMessage string, maxEvents int) error { +func (s *sqlDatabase) AddEntityEvent(ctx context.Context, entity params.ForgeEntity, event params.EventType, eventLevel params.EventLevel, statusMessage string, maxEvents int) error { if maxEvents == 0 { return errors.Wrap(runnerErrors.ErrBadRequest, "max events cannot be 0") } switch entity.EntityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: return s.addRepositoryEvent(ctx, entity.ID, event, eventLevel, statusMessage, maxEvents) - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: return s.addOrgEvent(ctx, entity.ID, event, eventLevel, statusMessage, maxEvents) - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: return s.addEnterpriseEvent(ctx, entity.ID, event, eventLevel, statusMessage, maxEvents) default: return errors.Wrap(runnerErrors.ErrBadRequest, "invalid entity type") } } + +func (s *sqlDatabase) sqlToCommonForgeCredentials(creds GithubCredentials) (params.ForgeCredentials, error) { + if len(creds.Payload) == 0 { + return params.ForgeCredentials{}, errors.New("empty credentials payload") + } + data, err := util.Unseal(creds.Payload, []byte(s.cfg.Passphrase)) + if err != nil { + return params.ForgeCredentials{}, errors.Wrap(err, "unsealing credentials") + } + + ep, err := s.sqlToCommonGithubEndpoint(creds.Endpoint) + if err != nil { + return params.ForgeCredentials{}, errors.Wrap(err, "converting github endpoint") + } + + commonCreds := params.ForgeCredentials{ + ID: creds.ID, + Name: creds.Name, + Description: creds.Description, + APIBaseURL: creds.Endpoint.APIBaseURL, + BaseURL: creds.Endpoint.BaseURL, + UploadBaseURL: creds.Endpoint.UploadBaseURL, + CABundle: creds.Endpoint.CACertBundle, + AuthType: creds.AuthType, + CreatedAt: creds.CreatedAt, + UpdatedAt: creds.UpdatedAt, + ForgeType: creds.Endpoint.EndpointType, + Endpoint: ep, + CredentialsPayload: data, + } + + for _, repo := range creds.Repositories { + commonRepo, err := s.sqlToCommonRepository(repo, false) + if err != nil { + return params.ForgeCredentials{}, errors.Wrap(err, "converting github repository") + } + commonCreds.Repositories = append(commonCreds.Repositories, commonRepo) + } + + for _, org := range creds.Organizations { + commonOrg, err := s.sqlToCommonOrganization(org, false) + if err != nil { + return params.ForgeCredentials{}, errors.Wrap(err, "converting github organization") + } + commonCreds.Organizations = append(commonCreds.Organizations, commonOrg) + } + + for _, ent := range creds.Enterprises { + commonEnt, err := s.sqlToCommonEnterprise(ent, false) + if err != nil { + return params.ForgeCredentials{}, errors.Wrapf(err, "converting github enterprise: %s", ent.Name) + } + commonCreds.Enterprises = append(commonCreds.Enterprises, commonEnt) + } + + return commonCreds, nil +} + +func (s *sqlDatabase) sqlGiteaToCommonForgeCredentials(creds GiteaCredentials) (params.ForgeCredentials, error) { + if len(creds.Payload) == 0 { + return params.ForgeCredentials{}, errors.New("empty credentials payload") + } + data, err := util.Unseal(creds.Payload, []byte(s.cfg.Passphrase)) + if err != nil { + return params.ForgeCredentials{}, errors.Wrap(err, "unsealing credentials") + } + + ep, err := s.sqlToCommonGithubEndpoint(creds.Endpoint) + if err != nil { + return params.ForgeCredentials{}, errors.Wrap(err, "converting github endpoint") + } + + commonCreds := params.ForgeCredentials{ + ID: creds.ID, + Name: creds.Name, + Description: creds.Description, + APIBaseURL: creds.Endpoint.APIBaseURL, + BaseURL: creds.Endpoint.BaseURL, + CABundle: creds.Endpoint.CACertBundle, + AuthType: creds.AuthType, + CreatedAt: creds.CreatedAt, + UpdatedAt: creds.UpdatedAt, + ForgeType: creds.Endpoint.EndpointType, + Endpoint: ep, + CredentialsPayload: data, + } + + for _, repo := range creds.Repositories { + commonRepo, err := s.sqlToCommonRepository(repo, false) + if err != nil { + return params.ForgeCredentials{}, errors.Wrap(err, "converting github repository") + } + commonCreds.Repositories = append(commonCreds.Repositories, commonRepo) + } + + for _, org := range creds.Organizations { + commonOrg, err := s.sqlToCommonOrganization(org, false) + if err != nil { + return params.ForgeCredentials{}, errors.Wrap(err, "converting github organization") + } + commonCreds.Organizations = append(commonCreds.Organizations, commonOrg) + } + + return commonCreds, nil +} + +func (s *sqlDatabase) sqlToCommonGithubEndpoint(ep GithubEndpoint) (params.ForgeEndpoint, error) { + return params.ForgeEndpoint{ + Name: ep.Name, + Description: ep.Description, + APIBaseURL: ep.APIBaseURL, + BaseURL: ep.BaseURL, + UploadBaseURL: ep.UploadBaseURL, + CACertBundle: ep.CACertBundle, + CreatedAt: ep.CreatedAt, + EndpointType: ep.EndpointType, + UpdatedAt: ep.UpdatedAt, + }, nil +} + +func getUIDFromContext(ctx context.Context) (uuid.UUID, error) { + userID := auth.UserID(ctx) + if userID == "" { + return uuid.Nil, errors.Wrap(runnerErrors.ErrUnauthorized, "getting UID from context") + } + + asUUID, err := uuid.Parse(userID) + if err != nil { + return uuid.Nil, errors.Wrap(runnerErrors.ErrUnauthorized, "parsing UID from context") + } + return asUUID, nil +} diff --git a/database/watcher/consumer.go b/database/watcher/consumer.go index 9282ece88..ed0967e92 100644 --- a/database/watcher/consumer.go +++ b/database/watcher/consumer.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package watcher import ( diff --git a/database/watcher/filters.go b/database/watcher/filters.go index dfcd54bb5..acf79ba89 100644 --- a/database/watcher/filters.go +++ b/database/watcher/filters.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package watcher import ( @@ -63,7 +77,7 @@ func WithOperationTypeFilter(operationType dbCommon.OperationType) dbCommon.Payl // WithEntityPoolFilter returns true if the change payload is a pool that belongs to the // supplied Github entity. This is useful when an entity worker wants to watch for changes // in pools that belong to it. -func WithEntityPoolFilter(ghEntity params.GithubEntity) dbCommon.PayloadFilterFunc { +func WithEntityPoolFilter(ghEntity params.ForgeEntity) dbCommon.PayloadFilterFunc { return func(payload dbCommon.ChangePayload) bool { switch payload.EntityType { case dbCommon.PoolEntityType: @@ -72,11 +86,11 @@ func WithEntityPoolFilter(ghEntity params.GithubEntity) dbCommon.PayloadFilterFu return false } switch ghEntity.EntityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: return pool.RepoID == ghEntity.ID - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: return pool.OrgID == ghEntity.ID - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: return pool.EnterpriseID == ghEntity.ID default: return false @@ -87,11 +101,20 @@ func WithEntityPoolFilter(ghEntity params.GithubEntity) dbCommon.PayloadFilterFu } } -// WithEntityPoolFilter returns true if the change payload is a pool that belongs to the -// supplied Github entity. This is useful when an entity worker wants to watch for changes -// in pools that belong to it. -func WithEntityScaleSetFilter(ghEntity params.GithubEntity) dbCommon.PayloadFilterFunc { +// WithEntityScaleSetFilter returns true if the change payload is a scale set that belongs to the +// supplied Github entity. +func WithEntityScaleSetFilter(ghEntity params.ForgeEntity) dbCommon.PayloadFilterFunc { return func(payload dbCommon.ChangePayload) bool { + forgeType, err := ghEntity.GetForgeType() + if err != nil { + return false + } + + // Gitea does not have scale sets. + if forgeType == params.GiteaEndpointType { + return false + } + switch payload.EntityType { case dbCommon.ScaleSetEntityType: scaleSet, ok := payload.Payload.(params.ScaleSet) @@ -99,11 +122,11 @@ func WithEntityScaleSetFilter(ghEntity params.GithubEntity) dbCommon.PayloadFilt return false } switch ghEntity.EntityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: return scaleSet.RepoID == ghEntity.ID - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: return scaleSet.OrgID == ghEntity.ID - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: return scaleSet.EnterpriseID == ghEntity.ID default: return false @@ -116,26 +139,26 @@ func WithEntityScaleSetFilter(ghEntity params.GithubEntity) dbCommon.PayloadFilt // WithEntityFilter returns a filter function that filters payloads by entity. // Change payloads that match the entity type and ID will return true. -func WithEntityFilter(entity params.GithubEntity) dbCommon.PayloadFilterFunc { +func WithEntityFilter(entity params.ForgeEntity) dbCommon.PayloadFilterFunc { return func(payload dbCommon.ChangePayload) bool { - if params.GithubEntityType(payload.EntityType) != entity.EntityType { + if params.ForgeEntityType(payload.EntityType) != entity.EntityType { return false } var ent IDGetter var ok bool switch payload.EntityType { case dbCommon.RepositoryEntityType: - if entity.EntityType != params.GithubEntityTypeRepository { + if entity.EntityType != params.ForgeEntityTypeRepository { return false } ent, ok = payload.Payload.(params.Repository) case dbCommon.OrganizationEntityType: - if entity.EntityType != params.GithubEntityTypeOrganization { + if entity.EntityType != params.ForgeEntityTypeOrganization { return false } ent, ok = payload.Payload.(params.Organization) case dbCommon.EnterpriseEntityType: - if entity.EntityType != params.GithubEntityTypeEnterprise { + if entity.EntityType != params.ForgeEntityTypeEnterprise { return false } ent, ok = payload.Payload.(params.Enterprise) @@ -149,7 +172,7 @@ func WithEntityFilter(entity params.GithubEntity) dbCommon.PayloadFilterFunc { } } -func WithEntityJobFilter(ghEntity params.GithubEntity) dbCommon.PayloadFilterFunc { +func WithEntityJobFilter(ghEntity params.ForgeEntity) dbCommon.PayloadFilterFunc { return func(payload dbCommon.ChangePayload) bool { switch payload.EntityType { case dbCommon.JobEntityType: @@ -159,15 +182,15 @@ func WithEntityJobFilter(ghEntity params.GithubEntity) dbCommon.PayloadFilterFun } switch ghEntity.EntityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: if job.RepoID != nil && job.RepoID.String() != ghEntity.ID { return false } - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: if job.OrgID != nil && job.OrgID.String() != ghEntity.ID { return false } - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: if job.EnterpriseID != nil && job.EnterpriseID.String() != ghEntity.ID { return false } @@ -182,17 +205,26 @@ func WithEntityJobFilter(ghEntity params.GithubEntity) dbCommon.PayloadFilterFun } } -// WithGithubCredentialsFilter returns a filter function that filters payloads by Github credentials. -func WithGithubCredentialsFilter(creds params.GithubCredentials) dbCommon.PayloadFilterFunc { +// WithForgeCredentialsFilter returns a filter function that filters payloads by Github or Gitea credentials. +func WithForgeCredentialsFilter(creds params.ForgeCredentials) dbCommon.PayloadFilterFunc { return func(payload dbCommon.ChangePayload) bool { - if payload.EntityType != dbCommon.GithubCredentialsEntityType { + var forgeCreds params.ForgeCredentials + var ok bool + switch payload.EntityType { + case dbCommon.GithubCredentialsEntityType, dbCommon.GiteaCredentialsEntityType: + forgeCreds, ok = payload.Payload.(params.ForgeCredentials) + default: return false } - credsPayload, ok := payload.Payload.(params.GithubCredentials) if !ok { return false } - return credsPayload.ID == creds.ID + // Gite and Github creds have different models. The ID is uint, so we + // need to explicitly check their type, or risk a clash. + if forgeCreds.ForgeType != creds.ForgeType { + return false + } + return forgeCreds.GetID() == creds.GetID() } } diff --git a/database/watcher/producer.go b/database/watcher/producer.go index 159ad8436..927aada06 100644 --- a/database/watcher/producer.go +++ b/database/watcher/producer.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package watcher import ( diff --git a/database/watcher/test_export.go b/database/watcher/test_export.go index f9b4ecf1a..eb3d38b6b 100644 --- a/database/watcher/test_export.go +++ b/database/watcher/test_export.go @@ -1,6 +1,19 @@ //go:build testing // +build testing +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package watcher import "github.com/cloudbase/garm/database/common" diff --git a/database/watcher/util_test.go b/database/watcher/util_test.go new file mode 100644 index 000000000..82b944914 --- /dev/null +++ b/database/watcher/util_test.go @@ -0,0 +1,16 @@ +package watcher_test + +import ( + "time" + + "github.com/cloudbase/garm/database/common" +) + +func waitForPayload(ch <-chan common.ChangePayload, timeout time.Duration) *common.ChangePayload { + select { + case payload := <-ch: + return &payload + case <-time.After(timeout): + return nil + } +} diff --git a/database/watcher/watcher.go b/database/watcher/watcher.go index fda318c60..a7e1cd672 100644 --- a/database/watcher/watcher.go +++ b/database/watcher/watcher.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package watcher import ( diff --git a/database/watcher/watcher_store_test.go b/database/watcher/watcher_store_test.go index 5a1486a89..e682270aa 100644 --- a/database/watcher/watcher_store_test.go +++ b/database/watcher/watcher_store_test.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package watcher_test import ( @@ -155,7 +169,7 @@ func (s *WatcherStoreTestSuite) TestInstanceWatcher() { creds := garmTesting.CreateTestGithubCredentials(s.ctx, "test-creds", s.store, s.T(), ep) s.T().Cleanup(func() { s.store.DeleteGithubCredentials(s.ctx, creds.ID) }) - repo, err := s.store.CreateRepository(s.ctx, "test-owner", "test-repo", creds.Name, "test-secret", params.PoolBalancerTypeRoundRobin) + repo, err := s.store.CreateRepository(s.ctx, "test-owner", "test-repo", creds, "test-secret", params.PoolBalancerTypeRoundRobin) s.Require().NoError(err) s.Require().NotEmpty(repo.ID) s.T().Cleanup(func() { s.store.DeleteRepository(s.ctx, repo.ID) }) @@ -259,7 +273,7 @@ func (s *WatcherStoreTestSuite) TestScaleSetInstanceWatcher() { creds := garmTesting.CreateTestGithubCredentials(s.ctx, "test-creds", s.store, s.T(), ep) s.T().Cleanup(func() { s.store.DeleteGithubCredentials(s.ctx, creds.ID) }) - repo, err := s.store.CreateRepository(s.ctx, "test-owner", "test-repo", creds.Name, "test-secret", params.PoolBalancerTypeRoundRobin) + repo, err := s.store.CreateRepository(s.ctx, "test-owner", "test-repo", creds, "test-secret", params.PoolBalancerTypeRoundRobin) s.Require().NoError(err) s.Require().NotEmpty(repo.ID) s.T().Cleanup(func() { s.store.DeleteRepository(s.ctx, repo.ID) }) @@ -369,7 +383,7 @@ func (s *WatcherStoreTestSuite) TestPoolWatcher() { } }) - repo, err := s.store.CreateRepository(s.ctx, "test-owner", "test-repo", creds.Name, "test-secret", params.PoolBalancerTypeRoundRobin) + repo, err := s.store.CreateRepository(s.ctx, "test-owner", "test-repo", creds, "test-secret", params.PoolBalancerTypeRoundRobin) s.Require().NoError(err) s.Require().NotEmpty(repo.ID) s.T().Cleanup(func() { s.store.DeleteRepository(s.ctx, repo.ID) }) @@ -490,7 +504,7 @@ func (s *WatcherStoreTestSuite) TestScaleSetWatcher() { } }) - repo, err := s.store.CreateRepository(s.ctx, "test-owner", "test-repo", creds.Name, "test-secret", params.PoolBalancerTypeRoundRobin) + repo, err := s.store.CreateRepository(s.ctx, "test-owner", "test-repo", creds, "test-secret", params.PoolBalancerTypeRoundRobin) s.Require().NoError(err) s.Require().NotEmpty(repo.ID) s.T().Cleanup(func() { s.store.DeleteRepository(s.ctx, repo.ID) }) @@ -646,7 +660,7 @@ func (s *WatcherStoreTestSuite) TestEnterpriseWatcher() { creds := garmTesting.CreateTestGithubCredentials(s.ctx, "test-creds", s.store, s.T(), ep) s.T().Cleanup(func() { s.store.DeleteGithubCredentials(s.ctx, creds.ID) }) - ent, err := s.store.CreateEnterprise(s.ctx, "test-enterprise", creds.Name, "test-secret", params.PoolBalancerTypeRoundRobin) + ent, err := s.store.CreateEnterprise(s.ctx, "test-enterprise", creds, "test-secret", params.PoolBalancerTypeRoundRobin) s.Require().NoError(err) s.Require().NotEmpty(ent.ID) @@ -713,7 +727,7 @@ func (s *WatcherStoreTestSuite) TestOrgWatcher() { creds := garmTesting.CreateTestGithubCredentials(s.ctx, "test-creds", s.store, s.T(), ep) s.T().Cleanup(func() { s.store.DeleteGithubCredentials(s.ctx, creds.ID) }) - org, err := s.store.CreateOrganization(s.ctx, "test-org", creds.Name, "test-secret", params.PoolBalancerTypeRoundRobin) + org, err := s.store.CreateOrganization(s.ctx, "test-org", creds, "test-secret", params.PoolBalancerTypeRoundRobin) s.Require().NoError(err) s.Require().NotEmpty(org.ID) @@ -780,7 +794,7 @@ func (s *WatcherStoreTestSuite) TestRepoWatcher() { creds := garmTesting.CreateTestGithubCredentials(s.ctx, "test-creds", s.store, s.T(), ep) s.T().Cleanup(func() { s.store.DeleteGithubCredentials(s.ctx, creds.ID) }) - repo, err := s.store.CreateRepository(s.ctx, "test-owner", "test-repo", creds.Name, "test-secret", params.PoolBalancerTypeRoundRobin) + repo, err := s.store.CreateRepository(s.ctx, "test-owner", "test-repo", creds, "test-secret", params.PoolBalancerTypeRoundRobin) s.Require().NoError(err) s.Require().NotEmpty(repo.ID) @@ -848,7 +862,7 @@ func (s *WatcherStoreTestSuite) TestGithubCredentialsWatcher() { Name: "test-creds", Description: "test credentials", Endpoint: "github.com", - AuthType: params.GithubAuthTypePAT, + AuthType: params.ForgeAuthTypePAT, PAT: params.GithubPAT{ OAuth2Token: "bogus", }, @@ -898,11 +912,103 @@ func (s *WatcherStoreTestSuite) TestGithubCredentialsWatcher() { EntityType: common.GithubCredentialsEntityType, Operation: common.DeleteOperation, // We only get the ID and Name of the deleted entity - Payload: params.GithubCredentials{ID: ghCred.ID, Name: ghCred.Name}, + Payload: params.ForgeCredentials{ID: ghCred.ID, Name: ghCred.Name}, + }, event) + case <-time.After(1 * time.Second): + s.T().Fatal("expected payload not received") + } +} + +func (s *WatcherStoreTestSuite) TestGiteaCredentialsWatcher() { + consumer, err := watcher.RegisterConsumer( + s.ctx, "gitea-cred-test", + watcher.WithEntityTypeFilter(common.GiteaCredentialsEntityType), + watcher.WithAny( + watcher.WithOperationTypeFilter(common.CreateOperation), + watcher.WithOperationTypeFilter(common.UpdateOperation), + watcher.WithOperationTypeFilter(common.DeleteOperation)), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + s.T().Cleanup(func() { consumer.Close() }) + consumeEvents(consumer) + + testEndpointParams := params.CreateGiteaEndpointParams{ + Name: "test", + Description: "test endpoint", + APIBaseURL: "https://api.gitea.example.com", + BaseURL: "https://gitea.example.com", + } + + testEndpoint, err := s.store.CreateGiteaEndpoint(s.ctx, testEndpointParams) + s.Require().NoError(err) + s.Require().NotEmpty(testEndpoint.Name) + + s.T().Cleanup(func() { + if err := s.store.DeleteGiteaEndpoint(s.ctx, testEndpoint.Name); err != nil { + s.T().Logf("failed to delete Gitea endpoint: %v", err) + } + consumeEvents(consumer) + }) + + giteaCredParams := params.CreateGiteaCredentialsParams{ + Name: "test-creds", + Description: "test credentials", + Endpoint: testEndpoint.Name, + AuthType: params.ForgeAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "bogus", + }, + } + + giteaCred, err := s.store.CreateGiteaCredentials(s.ctx, giteaCredParams) + s.Require().NoError(err) + s.Require().NotEmpty(giteaCred.ID) + + select { + case event := <-consumer.Watch(): + s.Require().Equal(common.ChangePayload{ + EntityType: common.GiteaCredentialsEntityType, + Operation: common.CreateOperation, + Payload: giteaCred, }, event) case <-time.After(1 * time.Second): s.T().Fatal("expected payload not received") } + + newDesc := "updated test description" + updateParams := params.UpdateGiteaCredentialsParams{ + Description: &newDesc, + } + + updatedGiteaCred, err := s.store.UpdateGiteaCredentials(s.ctx, giteaCred.ID, updateParams) + s.Require().NoError(err) + s.Require().Equal(newDesc, updatedGiteaCred.Description) + + select { + case event := <-consumer.Watch(): + s.Require().Equal(common.ChangePayload{ + EntityType: common.GiteaCredentialsEntityType, + Operation: common.UpdateOperation, + Payload: updatedGiteaCred, + }, event) + case <-time.After(1 * time.Second): + s.T().Fatal("expected payload not received") + } + + err = s.store.DeleteGiteaCredentials(s.ctx, giteaCred.ID) + s.Require().NoError(err) + + select { + case event := <-consumer.Watch(): + asCreds, ok := event.Payload.(params.ForgeCredentials) + s.Require().True(ok) + s.Require().Equal(event.Operation, common.DeleteOperation) + s.Require().Equal(event.EntityType, common.GiteaCredentialsEntityType) + s.Require().Equal(asCreds.ID, updatedGiteaCred.ID) + case <-time.After(1 * time.Second): + s.T().Fatal("expected payload not received") + } } func (s *WatcherStoreTestSuite) TestGithubEndpointWatcher() { @@ -971,7 +1077,7 @@ func (s *WatcherStoreTestSuite) TestGithubEndpointWatcher() { EntityType: common.GithubEndpointEntityType, Operation: common.DeleteOperation, // We only get the name of the deleted entity - Payload: params.GithubEndpoint{Name: ghEp.Name}, + Payload: params.ForgeEndpoint{Name: ghEp.Name}, }, event) case <-time.After(1 * time.Second): s.T().Fatal("expected payload not received") @@ -987,7 +1093,7 @@ consume: if !ok { return } - case <-time.After(100 * time.Millisecond): + case <-time.After(20 * time.Millisecond): break consume } } diff --git a/database/watcher/watcher_test.go b/database/watcher/watcher_test.go index c5b56fe20..5b7ecdced 100644 --- a/database/watcher/watcher_test.go +++ b/database/watcher/watcher_test.go @@ -1,5 +1,18 @@ //go:build testing +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package watcher_test import ( @@ -7,13 +20,16 @@ import ( "testing" "time" + "github.com/google/uuid" "github.com/pkg/errors" "github.com/stretchr/testify/suite" + commonParams "github.com/cloudbase/garm-provider-common/params" "github.com/cloudbase/garm/database" "github.com/cloudbase/garm/database/common" "github.com/cloudbase/garm/database/watcher" garmTesting "github.com/cloudbase/garm/internal/testing" + "github.com/cloudbase/garm/params" ) type WatcherTestSuite struct { @@ -37,6 +53,7 @@ func (s *WatcherTestSuite) TearDownTest() { currentWatcher := watcher.GetWatcher() if currentWatcher != nil { currentWatcher.Close() + watcher.SetWatcher(nil) } } @@ -44,6 +61,7 @@ func (s *WatcherTestSuite) TestRegisterConsumerTwiceWillError() { consumer, err := watcher.RegisterConsumer(s.ctx, "test") s.Require().NoError(err) s.Require().NotNil(consumer) + consumeEvents(consumer) consumer, err = watcher.RegisterConsumer(s.ctx, "test") s.Require().ErrorIs(err, common.ErrConsumerAlreadyRegistered) @@ -100,6 +118,7 @@ func (s *WatcherTestSuite) TestProducerAndConsumer() { watcher.WithOperationTypeFilter(common.UpdateOperation)) s.Require().NoError(err) s.Require().NotNil(consumer) + consumeEvents(consumer) payload := common.ChangePayload{ EntityType: common.ControllerEntityType, @@ -113,7 +132,7 @@ func (s *WatcherTestSuite) TestProducerAndConsumer() { s.Require().Equal(payload, receivedPayload) } -func (s *WatcherTestSuite) TestConsumetWithFilter() { +func (s *WatcherTestSuite) TestConsumeWithFilter() { producer, err := watcher.RegisterProducer(s.ctx, "test-producer") s.Require().NoError(err) s.Require().NotNil(producer) @@ -124,6 +143,7 @@ func (s *WatcherTestSuite) TestConsumetWithFilter() { watcher.WithOperationTypeFilter(common.UpdateOperation)) s.Require().NoError(err) s.Require().NotNil(consumer) + consumeEvents(consumer) payload := common.ChangePayload{ EntityType: common.ControllerEntityType, @@ -133,12 +153,9 @@ func (s *WatcherTestSuite) TestConsumetWithFilter() { err = producer.Notify(payload) s.Require().NoError(err) - select { - case receivedPayload := <-consumer.Watch(): - s.Require().Equal(payload, receivedPayload) - case <-time.After(1 * time.Second): - s.T().Fatal("expected payload not received") - } + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) payload = common.ChangePayload{ EntityType: common.ControllerEntityType, @@ -148,11 +165,143 @@ func (s *WatcherTestSuite) TestConsumetWithFilter() { err = producer.Notify(payload) s.Require().NoError(err) - select { - case <-consumer.Watch(): - s.T().Fatal("unexpected payload received") - case <-time.After(1 * time.Second): + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithAnyFilter() { + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithAny( + watcher.WithEntityTypeFilter(common.ControllerEntityType), + watcher.WithEntityFilter(params.ForgeEntity{ + EntityType: params.ForgeEntityTypeRepository, + Owner: "test", + Name: "test", + ID: "test", + }), + )) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.ControllerEntityType, + Operation: common.UpdateOperation, + Payload: "test", + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.RepositoryEntityType, + Operation: common.UpdateOperation, + Payload: params.Repository{ + Owner: "test", + Name: "test", + ID: "test", + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) + + // We're not watching for this repo + payload = common.ChangePayload{ + EntityType: common.RepositoryEntityType, + Operation: common.UpdateOperation, + Payload: params.Repository{ + Owner: "test", + Name: "test", + ID: "test2", + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) + + // We're not watching for orgs + payload = common.ChangePayload{ + EntityType: common.OrganizationEntityType, + Operation: common.UpdateOperation, + Payload: params.Repository{ + Owner: "test", + Name: "test", + ID: "test2", + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithAllFilter() { + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithAll( + watcher.WithEntityFilter(params.ForgeEntity{ + EntityType: params.ForgeEntityTypeRepository, + Owner: "test", + Name: "test", + ID: "test", + }), + watcher.WithOperationTypeFilter(common.CreateOperation), + )) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.RepositoryEntityType, + Operation: common.CreateOperation, + Payload: params.Repository{ + Owner: "test", + Name: "test", + ID: "test", + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.RepositoryEntityType, + Operation: common.UpdateOperation, + Payload: params.Repository{ + Owner: "test", + Name: "test", + ID: "test", + }, } + + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) } func maybeInitController(db common.Store) error { @@ -167,6 +316,1149 @@ func maybeInitController(db common.Store) error { return nil } +func (s *WatcherTestSuite) TestWithEntityPoolFilterRepository() { + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + entity := params.ForgeEntity{ + EntityType: params.ForgeEntityTypeRepository, + Owner: "test", + Name: "test", + ID: "test", + } + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithEntityPoolFilter(entity), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.PoolEntityType, + Operation: common.UpdateOperation, + Payload: params.Pool{ + ID: "test", + RepoID: "test", + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.PoolEntityType, + Operation: common.UpdateOperation, + Payload: params.Pool{ + ID: "test", + RepoID: "test2", + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithEntityPoolFilterOrg() { + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + entity := params.ForgeEntity{ + EntityType: params.ForgeEntityTypeOrganization, + Name: "test", + ID: "test", + } + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithEntityPoolFilter(entity), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.PoolEntityType, + Operation: common.UpdateOperation, + Payload: params.Pool{ + ID: "test", + OrgID: "test", + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.PoolEntityType, + Operation: common.UpdateOperation, + Payload: params.Pool{ + ID: "test", + OrgID: "test2", + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithEntityPoolFilterEnterprise() { + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + entity := params.ForgeEntity{ + EntityType: params.ForgeEntityTypeEnterprise, + Name: "test", + ID: "test", + } + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithEntityPoolFilter(entity), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.PoolEntityType, + Operation: common.UpdateOperation, + Payload: params.Pool{ + ID: "test", + EnterpriseID: "test", + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.PoolEntityType, + Operation: common.UpdateOperation, + Payload: params.Pool{ + ID: "test", + EnterpriseID: "test2", + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) + + // Invalid payload for declared entity type + payload = common.ChangePayload{ + EntityType: common.PoolEntityType, + Operation: common.UpdateOperation, + Payload: params.ScaleSet{ + ID: 1, + EnterpriseID: "test2", + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithEntityPoolFilterBogusEntityType() { + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + entity := params.ForgeEntity{ + // This should trigger the default branch in the filter and + // return false + EntityType: params.ForgeEntityType("bogus"), + Name: "test", + ID: "test", + } + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithEntityPoolFilter(entity), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.PoolEntityType, + Operation: common.UpdateOperation, + Payload: params.Pool{ + ID: "test", + EnterpriseID: "test", + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.PoolEntityType, + Operation: common.UpdateOperation, + Payload: params.Pool{ + ID: "test", + EnterpriseID: "test2", + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithEntityScaleSetFilterRepository() { + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + entity := params.ForgeEntity{ + EntityType: params.ForgeEntityTypeRepository, + Owner: "test", + Name: "test", + ID: "test", + Credentials: params.ForgeCredentials{ + ForgeType: params.GithubEndpointType, + }, + } + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithEntityScaleSetFilter(entity), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.ScaleSetEntityType, + Operation: common.UpdateOperation, + Payload: params.ScaleSet{ + ID: 1, + RepoID: "test", + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.ScaleSetEntityType, + Operation: common.UpdateOperation, + Payload: params.ScaleSet{ + ID: 1, + RepoID: "test2", + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithEntityScaleSetFilterOrg() { + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + entity := params.ForgeEntity{ + EntityType: params.ForgeEntityTypeOrganization, + Name: "test", + ID: "test", + Credentials: params.ForgeCredentials{ + ForgeType: params.GithubEndpointType, + }, + } + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithEntityScaleSetFilter(entity), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.ScaleSetEntityType, + Operation: common.UpdateOperation, + Payload: params.ScaleSet{ + ID: 1, + OrgID: "test", + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.ScaleSetEntityType, + Operation: common.UpdateOperation, + Payload: params.ScaleSet{ + ID: 1, + OrgID: "test2", + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithEntityScaleSetFilterEnterprise() { + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + entity := params.ForgeEntity{ + EntityType: params.ForgeEntityTypeEnterprise, + Name: "test", + ID: "test", + Credentials: params.ForgeCredentials{ + ForgeType: params.GithubEndpointType, + }, + } + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithEntityScaleSetFilter(entity), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.ScaleSetEntityType, + Operation: common.UpdateOperation, + Payload: params.ScaleSet{ + ID: 1, + EnterpriseID: "test", + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.ScaleSetEntityType, + Operation: common.UpdateOperation, + Payload: params.ScaleSet{ + ID: 1, + EnterpriseID: "test2", + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithEntityScaleSetFilterBogusEntityType() { + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + entity := params.ForgeEntity{ + // This should trigger the default branch in the filter and + // return false + EntityType: params.ForgeEntityType("bogus"), + Name: "test", + ID: "test", + Credentials: params.ForgeCredentials{ + ForgeType: params.GithubEndpointType, + }, + } + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithEntityScaleSetFilter(entity), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.ScaleSetEntityType, + Operation: common.UpdateOperation, + Payload: params.ScaleSet{ + ID: 1, + EnterpriseID: "test", + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.ScaleSetEntityType, + Operation: common.UpdateOperation, + Payload: params.ScaleSet{ + ID: 1, + EnterpriseID: "test2", + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithEntityScaleSetFilterReturnsFalseForGiteaEndpoints() { + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + entity := params.ForgeEntity{ + EntityType: params.ForgeEntityTypeRepository, + Owner: "test", + Name: "test", + ID: "test", + Credentials: params.ForgeCredentials{ + ForgeType: params.GiteaEndpointType, + }, + } + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithEntityScaleSetFilter(entity), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.ScaleSetEntityType, + Operation: common.UpdateOperation, + Payload: params.ScaleSet{ + ID: 1, + RepoID: "test", + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithEntityFilterRepository() { + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + entity := params.ForgeEntity{ + EntityType: params.ForgeEntityTypeRepository, + Owner: "test", + Name: "test", + ID: "test", + } + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithEntityFilter(entity), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.RepositoryEntityType, + Operation: common.UpdateOperation, + Payload: params.Repository{ + ID: "test", + Name: "test", + Owner: "test", + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.RepositoryEntityType, + Operation: common.UpdateOperation, + Payload: params.Repository{ + ID: "test2", + Name: "test", + Owner: "test", + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithEntityFilterOrg() { + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + entity := params.ForgeEntity{ + EntityType: params.ForgeEntityTypeOrganization, + Name: "test", + ID: "test", + } + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithEntityFilter(entity), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.OrganizationEntityType, + Operation: common.UpdateOperation, + Payload: params.Organization{ + ID: "test", + Name: "test", + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.OrganizationEntityType, + Operation: common.UpdateOperation, + Payload: params.Organization{ + ID: "test2", + Name: "test", + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithEntityFilterEnterprise() { + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + entity := params.ForgeEntity{ + EntityType: params.ForgeEntityTypeEnterprise, + Name: "test", + ID: "test", + } + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithEntityFilter(entity), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.EnterpriseEntityType, + Operation: common.UpdateOperation, + Payload: params.Enterprise{ + ID: "test", + Name: "test", + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.EnterpriseEntityType, + Operation: common.UpdateOperation, + Payload: params.Enterprise{ + ID: "test2", + Name: "test", + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithEntityJobFilterRepository() { + repoUUID, err := uuid.NewUUID() + s.Require().NoError(err) + + repoUUID2, err := uuid.NewUUID() + s.Require().NoError(err) + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + entity := params.ForgeEntity{ + EntityType: params.ForgeEntityTypeRepository, + Owner: "test", + Name: "test", + ID: repoUUID.String(), + } + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithEntityJobFilter(entity), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.JobEntityType, + Operation: common.UpdateOperation, + Payload: params.Job{ + ID: 1, + Name: "test", + RepoID: &repoUUID, + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.JobEntityType, + Operation: common.UpdateOperation, + Payload: params.Job{ + ID: 1, + Name: "test", + RepoID: &repoUUID2, + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithEntityJobFilterOrg() { + orgUUID, err := uuid.NewUUID() + s.Require().NoError(err) + + orgUUID2, err := uuid.NewUUID() + s.Require().NoError(err) + + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + entity := params.ForgeEntity{ + EntityType: params.ForgeEntityTypeOrganization, + Name: "test", + ID: orgUUID.String(), + } + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithEntityJobFilter(entity), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.JobEntityType, + Operation: common.UpdateOperation, + Payload: params.Job{ + ID: 1, + Name: "test", + OrgID: &orgUUID, + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.JobEntityType, + Operation: common.UpdateOperation, + Payload: params.Job{ + ID: 1, + Name: "test", + OrgID: &orgUUID2, + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithEntityJobFilterEnterprise() { + entUUID, err := uuid.NewUUID() + s.Require().NoError(err) + + entUUID2, err := uuid.NewUUID() + s.Require().NoError(err) + + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + entity := params.ForgeEntity{ + EntityType: params.ForgeEntityTypeEnterprise, + Name: "test", + ID: entUUID.String(), + } + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithEntityJobFilter(entity), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.JobEntityType, + Operation: common.UpdateOperation, + Payload: params.Job{ + ID: 1, + Name: "test", + EnterpriseID: &entUUID, + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.JobEntityType, + Operation: common.UpdateOperation, + Payload: params.Job{ + ID: 1, + Name: "test", + EnterpriseID: &entUUID2, + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithEntityJobFilterBogusEntityType() { + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + entity := params.ForgeEntity{ + // This should trigger the default branch in the filter and + // return false + EntityType: params.ForgeEntityType("bogus"), + Name: "test", + ID: "test", + } + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithEntityJobFilter(entity), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.JobEntityType, + Operation: common.UpdateOperation, + Payload: params.Job{ + ID: 1, + Name: "test", + EnterpriseID: nil, + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.JobEntityType, + Operation: common.UpdateOperation, + Payload: params.Job{ + ID: 1, + Name: "test", + EnterpriseID: nil, + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithNone() { + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithNone(), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.RepositoryEntityType, + Operation: common.UpdateOperation, + Payload: params.Repository{ + ID: "test", + Name: "test", + Owner: "test", + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithUserIDFilter() { + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + userID, err := uuid.NewUUID() + s.Require().NoError(err) + + userID2, err := uuid.NewUUID() + s.Require().NoError(err) + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithUserIDFilter(userID.String()), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.UserEntityType, + Operation: common.UpdateOperation, + Payload: params.User{ + ID: userID.String(), + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.UserEntityType, + Operation: common.UpdateOperation, + Payload: params.User{ + ID: userID2.String(), + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.UserEntityType, + Operation: common.UpdateOperation, + // Declare as user, but payload is a pool. Filter should return false. + Payload: params.Pool{}, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithForgeCredentialsGithub() { + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + creds := params.ForgeCredentials{ + ForgeType: params.GithubEndpointType, + ID: 1, + } + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithForgeCredentialsFilter(creds), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.GithubCredentialsEntityType, + Operation: common.UpdateOperation, + Payload: params.ForgeCredentials{ + ForgeType: params.GithubEndpointType, + ID: 1, + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.GiteaCredentialsEntityType, + Operation: common.UpdateOperation, + Payload: params.ForgeCredentials{ + ForgeType: params.GiteaEndpointType, + ID: 1, + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.GiteaCredentialsEntityType, + Operation: common.UpdateOperation, + Payload: params.Pool{}, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithcaleSetFilter() { + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + scaleSet := params.ScaleSet{ + ID: 1, + } + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithScaleSetFilter(scaleSet), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.ScaleSetEntityType, + Operation: common.UpdateOperation, + Payload: params.ScaleSet{ + ID: 1, + Name: "test", + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.ScaleSetEntityType, + Operation: common.UpdateOperation, + Payload: params.ScaleSet{ + ID: 2, + Name: "test", + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.ScaleSetEntityType, + Operation: common.UpdateOperation, + Payload: params.Pool{}, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) +} + +func (s *WatcherTestSuite) TestWithExcludeEntityTypeFilter() { + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithExcludeEntityTypeFilter(common.RepositoryEntityType), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.RepositoryEntityType, + Operation: common.UpdateOperation, + Payload: params.Repository{ + ID: "test", + Name: "test", + Owner: "test", + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.OrganizationEntityType, + Operation: common.UpdateOperation, + Payload: params.Repository{ + ID: "test", + Name: "test", + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) +} + +func (s *WatcherTestSuite) TestWithInstanceStatusFilter() { + producer, err := watcher.RegisterProducer(s.ctx, "test-producer") + s.Require().NoError(err) + s.Require().NotNil(producer) + + consumer, err := watcher.RegisterConsumer( + s.ctx, "test-consumer", + watcher.WithInstanceStatusFilter( + commonParams.InstanceCreating, + commonParams.InstanceDeleting), + ) + s.Require().NoError(err) + s.Require().NotNil(consumer) + consumeEvents(consumer) + + payload := common.ChangePayload{ + EntityType: common.InstanceEntityType, + Operation: common.UpdateOperation, + Payload: params.Instance{ + ID: "test-instance", + Status: commonParams.InstanceCreating, + }, + } + err = producer.Notify(payload) + s.Require().NoError(err) + + receivedPayload := waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.InstanceEntityType, + Operation: common.UpdateOperation, + Payload: params.Instance{ + ID: "test-instance", + Status: commonParams.InstanceDeleted, + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().Nil(receivedPayload) + + payload = common.ChangePayload{ + EntityType: common.InstanceEntityType, + Operation: common.UpdateOperation, + Payload: params.Instance{ + ID: "test-instance", + Status: commonParams.InstanceDeleting, + }, + } + + err = producer.Notify(payload) + s.Require().NoError(err) + receivedPayload = waitForPayload(consumer.Watch(), 100*time.Millisecond) + s.Require().NotNil(receivedPayload) + s.Require().Equal(payload, *receivedPayload) +} + func TestWatcherTestSuite(t *testing.T) { // Watcher tests watcherSuite := &WatcherTestSuite{ diff --git a/go.mod b/go.mod index a0b3901fa..5070dbfe0 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.10.0 golang.org/x/crypto v0.38.0 + golang.org/x/mod v0.17.0 golang.org/x/oauth2 v0.30.0 golang.org/x/sync v0.14.0 gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 diff --git a/go.sum b/go.sum index 3c9af9bb9..1cbc5ee0b 100644 --- a/go.sum +++ b/go.sum @@ -190,6 +190,8 @@ go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= diff --git a/internal/testing/mock_watcher.go b/internal/testing/mock_watcher.go index 67ae5da48..112f0de58 100644 --- a/internal/testing/mock_watcher.go +++ b/internal/testing/mock_watcher.go @@ -1,6 +1,20 @@ //go:build testing // +build testing +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package testing import ( diff --git a/internal/testing/testing.go b/internal/testing/testing.go index b3d049fdc..84b4d48c4 100644 --- a/internal/testing/testing.go +++ b/internal/testing/testing.go @@ -85,7 +85,7 @@ func CreateGARMTestUser(ctx context.Context, username string, db common.Store, s return user } -func CreateDefaultGithubEndpoint(ctx context.Context, db common.Store, s *testing.T) params.GithubEndpoint { +func CreateDefaultGithubEndpoint(ctx context.Context, db common.Store, s *testing.T) params.ForgeEndpoint { endpointParams := params.CreateGithubEndpointParams{ Name: "github.com", Description: "github endpoint", @@ -110,11 +110,35 @@ func CreateDefaultGithubEndpoint(ctx context.Context, db common.Store, s *testin return ep } -func CreateTestGithubCredentials(ctx context.Context, credsName string, db common.Store, s *testing.T, endpoint params.GithubEndpoint) params.GithubCredentials { +func CreateDefaultGiteaEndpoint(ctx context.Context, db common.Store, s *testing.T) params.ForgeEndpoint { + endpointParams := params.CreateGiteaEndpointParams{ + Name: "gitea.example.com", + Description: "gitea endpoint", + APIBaseURL: "https://gitea.example.com/", + BaseURL: "https://gitea.example.com/", + } + + ep, err := db.GetGithubEndpoint(ctx, endpointParams.Name) + if err != nil { + if !errors.Is(err, runnerErrors.ErrNotFound) { + s.Fatalf("failed to get database object (github.com): %v", err) + } + ep, err = db.CreateGiteaEndpoint(ctx, endpointParams) + if err != nil { + if !errors.Is(err, runnerErrors.ErrDuplicateEntity) { + s.Fatalf("failed to create database object (github.com): %v", err) + } + } + } + + return ep +} + +func CreateTestGithubCredentials(ctx context.Context, credsName string, db common.Store, s *testing.T, endpoint params.ForgeEndpoint) params.ForgeCredentials { newCredsParams := params.CreateGithubCredentialsParams{ Name: credsName, Description: "Test creds", - AuthType: params.GithubAuthTypePAT, + AuthType: params.ForgeAuthTypePAT, Endpoint: endpoint.Name, PAT: params.GithubPAT{ OAuth2Token: "test-token", @@ -127,6 +151,23 @@ func CreateTestGithubCredentials(ctx context.Context, credsName string, db commo return newCreds } +func CreateTestGiteaCredentials(ctx context.Context, credsName string, db common.Store, s *testing.T, endpoint params.ForgeEndpoint) params.ForgeCredentials { + newCredsParams := params.CreateGiteaCredentialsParams{ + Name: credsName, + Description: "Test creds", + AuthType: params.ForgeAuthTypePAT, + Endpoint: endpoint.Name, + PAT: params.GithubPAT{ + OAuth2Token: "test-token", + }, + } + newCreds, err := db.CreateGiteaCredentials(ctx, newCredsParams) + if err != nil { + s.Fatalf("failed to create database object (%s): %v", credsName, err) + } + return newCreds +} + func GetTestSqliteDBConfig(t *testing.T) config.Database { dir, err := os.MkdirTemp("", "garm-config-test") if err != nil { diff --git a/locking/interface.go b/locking/interface.go index 2b6ffb472..43ed17374 100644 --- a/locking/interface.go +++ b/locking/interface.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package locking import "time" diff --git a/locking/local_backoff_locker.go b/locking/local_backoff_locker.go index 9c2fecb15..933445666 100644 --- a/locking/local_backoff_locker.go +++ b/locking/local_backoff_locker.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package locking import ( diff --git a/locking/local_backoff_locker_test.go b/locking/local_backoff_locker_test.go index a9a986e21..00fe09c8f 100644 --- a/locking/local_backoff_locker_test.go +++ b/locking/local_backoff_locker_test.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package locking import ( diff --git a/locking/local_locker.go b/locking/local_locker.go index fc5ea847d..312d85eca 100644 --- a/locking/local_locker.go +++ b/locking/local_locker.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package locking import ( diff --git a/locking/local_locker_test.go b/locking/local_locker_test.go index 6decf512c..75b4dac09 100644 --- a/locking/local_locker_test.go +++ b/locking/local_locker_test.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package locking import ( diff --git a/locking/locking.go b/locking/locking.go index d485f5ff7..312d2e6a2 100644 --- a/locking/locking.go +++ b/locking/locking.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package locking import ( diff --git a/metrics/enterprise.go b/metrics/enterprise.go index f8382edfa..882b64dff 100644 --- a/metrics/enterprise.go +++ b/metrics/enterprise.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package metrics import ( diff --git a/metrics/github.go b/metrics/github.go index 0c0506520..0d6f5fa74 100644 --- a/metrics/github.go +++ b/metrics/github.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package metrics import "github.com/prometheus/client_golang/prometheus" diff --git a/metrics/health.go b/metrics/health.go index 4acfbb36c..13194231f 100644 --- a/metrics/health.go +++ b/metrics/health.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package metrics import ( diff --git a/metrics/instance.go b/metrics/instance.go index 7c2f2f968..b9d7e1cf1 100644 --- a/metrics/instance.go +++ b/metrics/instance.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package metrics import ( diff --git a/metrics/metrics.go b/metrics/metrics.go index edceb30a9..1a566116d 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package metrics import ( diff --git a/metrics/organization.go b/metrics/organization.go index 38d7c6110..d04e7a4ef 100644 --- a/metrics/organization.go +++ b/metrics/organization.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package metrics import ( diff --git a/metrics/pool.go b/metrics/pool.go index 5803af90d..fc6f2520b 100644 --- a/metrics/pool.go +++ b/metrics/pool.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package metrics import ( diff --git a/metrics/provider.go b/metrics/provider.go index 8285ca1e6..3262ab3bf 100644 --- a/metrics/provider.go +++ b/metrics/provider.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package metrics import ( diff --git a/metrics/repository.go b/metrics/repository.go index a84dd1203..21714233e 100644 --- a/metrics/repository.go +++ b/metrics/repository.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package metrics import ( diff --git a/metrics/util.go b/metrics/util.go index b2edb5808..d83b49730 100644 --- a/metrics/util.go +++ b/metrics/util.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package metrics func Bool2float64(b bool) float64 { diff --git a/metrics/webhooks.go b/metrics/webhooks.go index 839219a4c..48a08f9c8 100644 --- a/metrics/webhooks.go +++ b/metrics/webhooks.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package metrics import "github.com/prometheus/client_golang/prometheus" diff --git a/params/github.go b/params/github.go index 0f9630902..9859f717d 100644 --- a/params/github.go +++ b/params/github.go @@ -171,7 +171,9 @@ type WorkflowJob struct { DefaultBranch string `json:"default_branch"` } `json:"repository"` Organization struct { - Login string `json:"login"` + Login string `json:"login"` + // Name is a gitea specific field + Name string `json:"name"` ID int64 `json:"id"` NodeID string `json:"node_id"` URL string `json:"url"` @@ -218,6 +220,13 @@ type WorkflowJob struct { } `json:"sender"` } +func (w WorkflowJob) GetOrgName(forgeType EndpointType) string { + if forgeType == GiteaEndpointType { + return w.Organization.Name + } + return w.Organization.Login +} + type RunnerSetting struct { Ephemeral bool `json:"ephemeral,omitempty"` IsElastic bool `json:"isElastic,omitempty"` diff --git a/params/interfaces.go b/params/interfaces.go index cd9b94ff6..31ef635f8 100644 --- a/params/interfaces.go +++ b/params/interfaces.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package params import "time" @@ -5,7 +19,7 @@ import "time" // EntityGetter is implemented by all github entities (repositories, organizations and enterprises). // It defines the GetEntity() function which returns a github entity. type EntityGetter interface { - GetEntity() (GithubEntity, error) + GetEntity() (ForgeEntity, error) } type IDGetter interface { @@ -15,3 +29,7 @@ type IDGetter interface { type CreationDateGetter interface { GetCreatedAt() time.Time } + +type ForgeCredentialsGetter interface { + GetForgeCredentials() ForgeCredentials +} diff --git a/params/params.go b/params/params.go index 7636102f1..e154b2df0 100644 --- a/params/params.go +++ b/params/params.go @@ -36,14 +36,15 @@ import ( ) type ( - GithubEntityType string + ForgeEntityType string EventType string EventLevel string ProviderType string JobStatus string RunnerStatus string WebhookEndpointType string - GithubAuthType string + ForgeAuthType string + EndpointType string PoolBalancerType string ScaleSetState string ScaleSetMessageType string @@ -76,6 +77,11 @@ const ( PoolBalancerTypeNone PoolBalancerType = "" ) +const ( + GithubEndpointType EndpointType = "github" + GiteaEndpointType EndpointType = "gitea" +) + const ( // LXDProvider represents the LXD provider. LXDProvider ProviderType = "lxd" @@ -100,9 +106,9 @@ const ( ) const ( - GithubEntityTypeRepository GithubEntityType = "repository" - GithubEntityTypeOrganization GithubEntityType = "organization" - GithubEntityTypeEnterprise GithubEntityType = "enterprise" + ForgeEntityTypeRepository ForgeEntityType = "repository" + ForgeEntityTypeOrganization ForgeEntityType = "organization" + ForgeEntityTypeEnterprise ForgeEntityType = "enterprise" ) const ( @@ -135,13 +141,13 @@ const ( ) const ( - // GithubAuthTypePAT is the OAuth token based authentication - GithubAuthTypePAT GithubAuthType = "pat" - // GithubAuthTypeApp is the GitHub App based authentication - GithubAuthTypeApp GithubAuthType = "app" + // ForgeAuthTypePAT is the OAuth token based authentication + ForgeAuthTypePAT ForgeAuthType = "pat" + // ForgeAuthTypeApp is the GitHub App based authentication + ForgeAuthTypeApp ForgeAuthType = "app" ) -func (e GithubEntityType) String() string { +func (e ForgeEntityType) String() string { return string(e) } @@ -338,26 +344,30 @@ type Tag struct { type Pool struct { RunnerPrefix - ID string `json:"id,omitempty"` - ProviderName string `json:"provider_name,omitempty"` - MaxRunners uint `json:"max_runners,omitempty"` - MinIdleRunners uint `json:"min_idle_runners,omitempty"` - Image string `json:"image,omitempty"` - Flavor string `json:"flavor,omitempty"` - OSType commonParams.OSType `json:"os_type,omitempty"` - OSArch commonParams.OSArch `json:"os_arch,omitempty"` - Tags []Tag `json:"tags,omitempty"` - Enabled bool `json:"enabled,omitempty"` - Instances []Instance `json:"instances,omitempty"` - RepoID string `json:"repo_id,omitempty"` - RepoName string `json:"repo_name,omitempty"` - OrgID string `json:"org_id,omitempty"` - OrgName string `json:"org_name,omitempty"` - EnterpriseID string `json:"enterprise_id,omitempty"` - EnterpriseName string `json:"enterprise_name,omitempty"` - RunnerBootstrapTimeout uint `json:"runner_bootstrap_timeout,omitempty"` - CreatedAt time.Time `json:"created_at,omitempty"` - UpdatedAt time.Time `json:"updated_at,omitempty"` + ID string `json:"id,omitempty"` + ProviderName string `json:"provider_name,omitempty"` + MaxRunners uint `json:"max_runners,omitempty"` + MinIdleRunners uint `json:"min_idle_runners,omitempty"` + Image string `json:"image,omitempty"` + Flavor string `json:"flavor,omitempty"` + OSType commonParams.OSType `json:"os_type,omitempty"` + OSArch commonParams.OSArch `json:"os_arch,omitempty"` + Tags []Tag `json:"tags,omitempty"` + Enabled bool `json:"enabled,omitempty"` + Instances []Instance `json:"instances,omitempty"` + + RepoID string `json:"repo_id,omitempty"` + RepoName string `json:"repo_name,omitempty"` + + OrgID string `json:"org_id,omitempty"` + OrgName string `json:"org_name,omitempty"` + + EnterpriseID string `json:"enterprise_id,omitempty"` + EnterpriseName string `json:"enterprise_name,omitempty"` + + RunnerBootstrapTimeout uint `json:"runner_bootstrap_timeout,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` // ExtraSpecs is an opaque raw json that gets sent to the provider // as part of the bootstrap params for instances. It can contain // any kind of data needed by providers. The contents of this field means @@ -374,13 +384,13 @@ type Pool struct { Priority uint `json:"priority,omitempty"` } -func (p Pool) BelongsTo(entity GithubEntity) bool { +func (p Pool) BelongsTo(entity ForgeEntity) bool { switch p.PoolType() { - case GithubEntityTypeRepository: + case ForgeEntityTypeRepository: return p.RepoID == entity.ID - case GithubEntityTypeOrganization: + case ForgeEntityTypeOrganization: return p.OrgID == entity.ID - case GithubEntityTypeEnterprise: + case ForgeEntityTypeEnterprise: return p.EnterpriseID == entity.ID } return false @@ -405,25 +415,25 @@ func (p Pool) MaxRunnersAsInt() int { return int(p.MaxRunners) } -func (p Pool) GetEntity() (GithubEntity, error) { +func (p Pool) GetEntity() (ForgeEntity, error) { switch p.PoolType() { - case GithubEntityTypeRepository: - return GithubEntity{ + case ForgeEntityTypeRepository: + return ForgeEntity{ ID: p.RepoID, - EntityType: GithubEntityTypeRepository, + EntityType: ForgeEntityTypeRepository, }, nil - case GithubEntityTypeOrganization: - return GithubEntity{ + case ForgeEntityTypeOrganization: + return ForgeEntity{ ID: p.OrgID, - EntityType: GithubEntityTypeOrganization, + EntityType: ForgeEntityTypeOrganization, }, nil - case GithubEntityTypeEnterprise: - return GithubEntity{ + case ForgeEntityTypeEnterprise: + return ForgeEntity{ ID: p.EnterpriseID, - EntityType: GithubEntityTypeEnterprise, + EntityType: ForgeEntityTypeEnterprise, }, nil } - return GithubEntity{}, fmt.Errorf("pool has no associated entity") + return ForgeEntity{}, fmt.Errorf("pool has no associated entity") } func (p Pool) GetID() string { @@ -437,14 +447,14 @@ func (p *Pool) RunnerTimeout() uint { return p.RunnerBootstrapTimeout } -func (p *Pool) PoolType() GithubEntityType { +func (p *Pool) PoolType() ForgeEntityType { switch { case p.RepoID != "": - return GithubEntityTypeRepository + return ForgeEntityTypeRepository case p.OrgID != "": - return GithubEntityTypeOrganization + return ForgeEntityTypeOrganization case p.EnterpriseID != "": - return GithubEntityTypeEnterprise + return ForgeEntityTypeEnterprise } return "" } @@ -513,13 +523,13 @@ type ScaleSet struct { LastMessageID int64 `json:"-"` } -func (p ScaleSet) BelongsTo(entity GithubEntity) bool { +func (p ScaleSet) BelongsTo(entity ForgeEntity) bool { switch p.ScaleSetType() { - case GithubEntityTypeRepository: + case ForgeEntityTypeRepository: return p.RepoID == entity.ID - case GithubEntityTypeOrganization: + case ForgeEntityTypeOrganization: return p.OrgID == entity.ID - case GithubEntityTypeEnterprise: + case ForgeEntityTypeEnterprise: return p.EnterpriseID == entity.ID } return false @@ -529,35 +539,35 @@ func (p ScaleSet) GetID() uint { return p.ID } -func (p ScaleSet) GetEntity() (GithubEntity, error) { +func (p ScaleSet) GetEntity() (ForgeEntity, error) { switch p.ScaleSetType() { - case GithubEntityTypeRepository: - return GithubEntity{ + case ForgeEntityTypeRepository: + return ForgeEntity{ ID: p.RepoID, - EntityType: GithubEntityTypeRepository, + EntityType: ForgeEntityTypeRepository, }, nil - case GithubEntityTypeOrganization: - return GithubEntity{ + case ForgeEntityTypeOrganization: + return ForgeEntity{ ID: p.OrgID, - EntityType: GithubEntityTypeOrganization, + EntityType: ForgeEntityTypeOrganization, }, nil - case GithubEntityTypeEnterprise: - return GithubEntity{ + case ForgeEntityTypeEnterprise: + return ForgeEntity{ ID: p.EnterpriseID, - EntityType: GithubEntityTypeEnterprise, + EntityType: ForgeEntityTypeEnterprise, }, nil } - return GithubEntity{}, fmt.Errorf("pool has no associated entity") + return ForgeEntity{}, fmt.Errorf("pool has no associated entity") } -func (p *ScaleSet) ScaleSetType() GithubEntityType { +func (p *ScaleSet) ScaleSetType() ForgeEntityType { switch { case p.RepoID != "": - return GithubEntityTypeRepository + return ForgeEntityTypeRepository case p.OrgID != "": - return GithubEntityTypeOrganization + return ForgeEntityTypeOrganization case p.EnterpriseID != "": - return GithubEntityTypeEnterprise + return ForgeEntityTypeEnterprise } return "" } @@ -580,35 +590,45 @@ type Repository struct { // CredentialName is the name of the credentials associated with the enterprise. // This field is now deprecated. Use CredentialsID instead. This field will be // removed in v0.2.0. - CredentialsName string `json:"credentials_name,omitempty"` - CredentialsID uint `json:"credentials_id,omitempty"` - Credentials GithubCredentials `json:"credentials,omitempty"` + CredentialsName string `json:"credentials_name,omitempty"` + + CredentialsID uint `json:"credentials_id,omitempty"` + Credentials ForgeCredentials `json:"credentials,omitempty"` + PoolManagerStatus PoolManagerStatus `json:"pool_manager_status,omitempty"` PoolBalancerType PoolBalancerType `json:"pool_balancing_type,omitempty"` - Endpoint GithubEndpoint `json:"endpoint,omitempty"` + Endpoint ForgeEndpoint `json:"endpoint,omitempty"` CreatedAt time.Time `json:"created_at,omitempty"` UpdatedAt time.Time `json:"updated_at,omitempty"` // Do not serialize sensitive info. WebhookSecret string `json:"-"` } +func (r Repository) GetCredentialsName() string { + if r.CredentialsName != "" { + return r.CredentialsName + } + return r.Credentials.Name +} + func (r Repository) CreationDateGetter() time.Time { return r.CreatedAt } -func (r Repository) GetEntity() (GithubEntity, error) { +func (r Repository) GetEntity() (ForgeEntity, error) { if r.ID == "" { - return GithubEntity{}, fmt.Errorf("repository has no ID") + return ForgeEntity{}, fmt.Errorf("repository has no ID") } - return GithubEntity{ + return ForgeEntity{ ID: r.ID, - EntityType: GithubEntityTypeRepository, + EntityType: ForgeEntityTypeRepository, Owner: r.Owner, Name: r.Name, PoolBalancerType: r.PoolBalancerType, Credentials: r.Credentials, WebhookSecret: r.WebhookSecret, CreatedAt: r.CreatedAt, + UpdatedAt: r.UpdatedAt, }, nil } @@ -642,11 +662,11 @@ type Organization struct { // This field is now deprecated. Use CredentialsID instead. This field will be // removed in v0.2.0. CredentialsName string `json:"credentials_name,omitempty"` - Credentials GithubCredentials `json:"credentials,omitempty"` + Credentials ForgeCredentials `json:"credentials,omitempty"` CredentialsID uint `json:"credentials_id,omitempty"` PoolManagerStatus PoolManagerStatus `json:"pool_manager_status,omitempty"` PoolBalancerType PoolBalancerType `json:"pool_balancing_type,omitempty"` - Endpoint GithubEndpoint `json:"endpoint,omitempty"` + Endpoint ForgeEndpoint `json:"endpoint,omitempty"` CreatedAt time.Time `json:"created_at,omitempty"` UpdatedAt time.Time `json:"updated_at,omitempty"` // Do not serialize sensitive info. @@ -657,18 +677,19 @@ func (o Organization) GetCreatedAt() time.Time { return o.CreatedAt } -func (o Organization) GetEntity() (GithubEntity, error) { +func (o Organization) GetEntity() (ForgeEntity, error) { if o.ID == "" { - return GithubEntity{}, fmt.Errorf("organization has no ID") + return ForgeEntity{}, fmt.Errorf("organization has no ID") } - return GithubEntity{ + return ForgeEntity{ ID: o.ID, - EntityType: GithubEntityTypeOrganization, + EntityType: ForgeEntityTypeOrganization, Owner: o.Name, WebhookSecret: o.WebhookSecret, PoolBalancerType: o.PoolBalancerType, Credentials: o.Credentials, CreatedAt: o.CreatedAt, + UpdatedAt: o.UpdatedAt, }, nil } @@ -698,11 +719,11 @@ type Enterprise struct { // This field is now deprecated. Use CredentialsID instead. This field will be // removed in v0.2.0. CredentialsName string `json:"credentials_name,omitempty"` - Credentials GithubCredentials `json:"credentials,omitempty"` + Credentials ForgeCredentials `json:"credentials,omitempty"` CredentialsID uint `json:"credentials_id,omitempty"` PoolManagerStatus PoolManagerStatus `json:"pool_manager_status,omitempty"` PoolBalancerType PoolBalancerType `json:"pool_balancing_type,omitempty"` - Endpoint GithubEndpoint `json:"endpoint,omitempty"` + Endpoint ForgeEndpoint `json:"endpoint,omitempty"` CreatedAt time.Time `json:"created_at,omitempty"` UpdatedAt time.Time `json:"updated_at,omitempty"` // Do not serialize sensitive info. @@ -713,18 +734,19 @@ func (e Enterprise) GetCreatedAt() time.Time { return e.CreatedAt } -func (e Enterprise) GetEntity() (GithubEntity, error) { +func (e Enterprise) GetEntity() (ForgeEntity, error) { if e.ID == "" { - return GithubEntity{}, fmt.Errorf("enterprise has no ID") + return ForgeEntity{}, fmt.Errorf("enterprise has no ID") } - return GithubEntity{ + return ForgeEntity{ ID: e.ID, - EntityType: GithubEntityTypeEnterprise, + EntityType: ForgeEntityTypeEnterprise, Owner: e.Name, WebhookSecret: e.WebhookSecret, PoolBalancerType: e.PoolBalancerType, Credentials: e.Credentials, CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, }, nil } @@ -837,33 +859,35 @@ func (g GithubRateLimit) ResetAt() time.Time { return time.Unix(g.Reset, 0) } -type GithubCredentials struct { - ID uint `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - APIBaseURL string `json:"api_base_url,omitempty"` - UploadBaseURL string `json:"upload_base_url,omitempty"` - BaseURL string `json:"base_url,omitempty"` - CABundle []byte `json:"ca_bundle,omitempty"` - AuthType GithubAuthType `json:"auth-type,omitempty"` - - Repositories []Repository `json:"repositories,omitempty"` - Organizations []Organization `json:"organizations,omitempty"` - Enterprises []Enterprise `json:"enterprises,omitempty"` - Endpoint GithubEndpoint `json:"endpoint,omitempty"` - CreatedAt time.Time `json:"created_at,omitempty"` - UpdatedAt time.Time `json:"updated_at,omitempty"` - RateLimit GithubRateLimit `json:"rate_limit,omitempty"` +type ForgeCredentials struct { + ID uint `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + APIBaseURL string `json:"api_base_url,omitempty"` + UploadBaseURL string `json:"upload_base_url,omitempty"` + BaseURL string `json:"base_url,omitempty"` + CABundle []byte `json:"ca_bundle,omitempty"` + AuthType ForgeAuthType `json:"auth-type,omitempty"` + + ForgeType EndpointType `json:"forge_type,omitempty"` + + Repositories []Repository `json:"repositories,omitempty"` + Organizations []Organization `json:"organizations,omitempty"` + Enterprises []Enterprise `json:"enterprises,omitempty"` + Endpoint ForgeEndpoint `json:"endpoint,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` + RateLimit *GithubRateLimit `json:"rate_limit,omitempty"` // Do not serialize sensitive info. CredentialsPayload []byte `json:"-"` } -func (g GithubCredentials) GetID() uint { +func (g ForgeCredentials) GetID() uint { return g.ID } -func (g GithubCredentials) GetHTTPClient(ctx context.Context) (*http.Client, error) { +func (g ForgeCredentials) GetHTTPClient(ctx context.Context) (*http.Client, error) { var roots *x509.CertPool if g.CABundle != nil { roots = x509.NewCertPool() @@ -882,7 +906,7 @@ func (g GithubCredentials) GetHTTPClient(ctx context.Context) (*http.Client, err var tc *http.Client switch g.AuthType { - case GithubAuthTypeApp: + case ForgeAuthTypeApp: var app GithubApp if err := json.Unmarshal(g.CredentialsPayload, &app); err != nil { return nil, fmt.Errorf("failed to unmarshal github app credentials: %w", err) @@ -918,7 +942,7 @@ func (g GithubCredentials) GetHTTPClient(ctx context.Context) (*http.Client, err return tc, nil } -func (g GithubCredentials) RootCertificateBundle() (CertificateBundle, error) { +func (g ForgeCredentials) RootCertificateBundle() (CertificateBundle, error) { if len(g.CABundle) == 0 { return CertificateBundle{}, nil } @@ -949,7 +973,7 @@ func (g GithubCredentials) RootCertificateBundle() (CertificateBundle, error) { } // used by swagger client generated code -type Credentials []GithubCredentials +type Credentials []ForgeCredentials type Provider struct { Name string `json:"name,omitempty"` @@ -1057,64 +1081,77 @@ type UpdateSystemInfoParams struct { AgentID *int64 `json:"agent_id,omitempty"` } -type GithubEntity struct { - Owner string `json:"owner,omitempty"` - Name string `json:"name,omitempty"` - ID string `json:"id,omitempty"` - EntityType GithubEntityType `json:"entity_type,omitempty"` - Credentials GithubCredentials `json:"credentials,omitempty"` - PoolBalancerType PoolBalancerType `json:"pool_balancing_type,omitempty"` - CreatedAt time.Time `json:"created_at,omitempty"` +type ForgeEntity struct { + Owner string `json:"owner,omitempty"` + Name string `json:"name,omitempty"` + ID string `json:"id,omitempty"` + EntityType ForgeEntityType `json:"entity_type,omitempty"` + Credentials ForgeCredentials `json:"credentials,omitempty"` + PoolBalancerType PoolBalancerType `json:"pool_balancing_type,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` WebhookSecret string `json:"-"` } -func (g GithubEntity) GetCreatedAt() time.Time { +func (g ForgeEntity) GetCreatedAt() time.Time { return g.CreatedAt } -func (g GithubEntity) GithubURL() string { - switch g.EntityType { - case GithubEntityTypeRepository: - return fmt.Sprintf("%s/%s/%s", g.Credentials.BaseURL, g.Owner, g.Name) - case GithubEntityTypeOrganization: - return fmt.Sprintf("%s/%s", g.Credentials.BaseURL, g.Owner) - case GithubEntityTypeEnterprise: - return fmt.Sprintf("%s/enterprises/%s", g.Credentials.BaseURL, g.Owner) +func (g ForgeEntity) GetForgeType() (EndpointType, error) { + if g.Credentials.ForgeType == "" { + return "", fmt.Errorf("credentials forge type is empty") + } + return g.Credentials.ForgeType, nil +} + +func (g ForgeEntity) ForgeURL() string { + switch g.Credentials.ForgeType { + case GiteaEndpointType: + return g.Credentials.Endpoint.APIBaseURL + default: + switch g.EntityType { + case ForgeEntityTypeRepository: + return fmt.Sprintf("%s/%s/%s", g.Credentials.BaseURL, g.Owner, g.Name) + case ForgeEntityTypeOrganization: + return fmt.Sprintf("%s/%s", g.Credentials.BaseURL, g.Owner) + case ForgeEntityTypeEnterprise: + return fmt.Sprintf("%s/enterprises/%s", g.Credentials.BaseURL, g.Owner) + } } return "" } -func (g GithubEntity) GetPoolBalancerType() PoolBalancerType { +func (g ForgeEntity) GetPoolBalancerType() PoolBalancerType { if g.PoolBalancerType == "" { return PoolBalancerTypeRoundRobin } return g.PoolBalancerType } -func (g GithubEntity) LabelScope() string { +func (g ForgeEntity) LabelScope() string { switch g.EntityType { - case GithubEntityTypeRepository: + case ForgeEntityTypeRepository: return MetricsLabelRepositoryScope - case GithubEntityTypeOrganization: + case ForgeEntityTypeOrganization: return MetricsLabelOrganizationScope - case GithubEntityTypeEnterprise: + case ForgeEntityTypeEnterprise: return MetricsLabelEnterpriseScope } return "" } -func (g GithubEntity) String() string { +func (g ForgeEntity) String() string { switch g.EntityType { - case GithubEntityTypeRepository: + case ForgeEntityTypeRepository: return fmt.Sprintf("%s/%s", g.Owner, g.Name) - case GithubEntityTypeOrganization, GithubEntityTypeEnterprise: + case ForgeEntityTypeOrganization, ForgeEntityTypeEnterprise: return g.Owner } return "" } -func (g GithubEntity) GetIDAsUUID() (uuid.UUID, error) { +func (g ForgeEntity) GetIDAsUUID() (uuid.UUID, error) { if g.ID == "" { return uuid.Nil, nil } @@ -1126,9 +1163,9 @@ func (g GithubEntity) GetIDAsUUID() (uuid.UUID, error) { } // used by swagger client generated code -type GithubEndpoints []GithubEndpoint +type ForgeEndpoints []ForgeEndpoint -type GithubEndpoint struct { +type ForgeEndpoint struct { Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` APIBaseURL string `json:"api_base_url,omitempty"` @@ -1138,5 +1175,5 @@ type GithubEndpoint struct { CreatedAt time.Time `json:"created_at,omitempty"` UpdatedAt time.Time `json:"updated_at,omitempty"` - Credentials []GithubCredentials `json:"credentials,omitempty"` + EndpointType EndpointType `json:"endpoint_type,omitempty"` } diff --git a/params/requests.go b/params/requests.go index 1166418f0..82cbf1138 100644 --- a/params/requests.go +++ b/params/requests.go @@ -45,6 +45,16 @@ type CreateRepoParams struct { CredentialsName string `json:"credentials_name,omitempty"` WebhookSecret string `json:"webhook_secret,omitempty"` PoolBalancerType PoolBalancerType `json:"pool_balancer_type,omitempty"` + ForgeType EndpointType `json:"forge_type,omitempty"` +} + +func (c CreateRepoParams) GetForgeType() EndpointType { + switch c.ForgeType { + case GithubEndpointType, GiteaEndpointType: + return c.ForgeType + default: + return GithubEndpointType + } } func (c *CreateRepoParams) Validate() error { @@ -77,6 +87,16 @@ type CreateOrgParams struct { CredentialsName string `json:"credentials_name,omitempty"` WebhookSecret string `json:"webhook_secret,omitempty"` PoolBalancerType PoolBalancerType `json:"pool_balancer_type,omitempty"` + ForgeType EndpointType `json:"forge_type,omitempty"` +} + +func (c CreateOrgParams) GetForgeType() EndpointType { + switch c.ForgeType { + case GithubEndpointType, GiteaEndpointType: + return c.ForgeType + default: + return GithubEndpointType + } } func (c *CreateOrgParams) Validate() error { @@ -437,12 +457,12 @@ func (g GithubApp) Validate() error { } type CreateGithubCredentialsParams struct { - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - Endpoint string `json:"endpoint,omitempty"` - AuthType GithubAuthType `json:"auth_type,omitempty"` - PAT GithubPAT `json:"pat,omitempty"` - App GithubApp `json:"app,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Endpoint string `json:"endpoint,omitempty"` + AuthType ForgeAuthType `json:"auth_type,omitempty"` + PAT GithubPAT `json:"pat,omitempty"` + App GithubApp `json:"app,omitempty"` } func (c CreateGithubCredentialsParams) Validate() error { @@ -455,18 +475,18 @@ func (c CreateGithubCredentialsParams) Validate() error { } switch c.AuthType { - case GithubAuthTypePAT, GithubAuthTypeApp: + case ForgeAuthTypePAT, ForgeAuthTypeApp: default: return runnerErrors.NewBadRequestError("invalid auth_type") } - if c.AuthType == GithubAuthTypePAT { + if c.AuthType == ForgeAuthTypePAT { if c.PAT.OAuth2Token == "" { return runnerErrors.NewBadRequestError("missing oauth2_token") } } - if c.AuthType == GithubAuthTypeApp { + if c.AuthType == ForgeAuthTypeApp { if err := c.App.Validate(); err != nil { return errors.Wrap(err, "invalid app") } @@ -606,3 +626,154 @@ type UpdateScaleSetParams struct { State *ScaleSetState `json:"state"` ExtendedState *string `json:"extended_state"` } + +type CreateGiteaEndpointParams struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + APIBaseURL string `json:"api_base_url,omitempty"` + BaseURL string `json:"base_url,omitempty"` + CACertBundle []byte `json:"ca_cert_bundle,omitempty"` +} + +func (c CreateGiteaEndpointParams) Validate() error { + if c.APIBaseURL == "" { + return runnerErrors.NewBadRequestError("missing api_base_url") + } + + url, err := url.Parse(c.APIBaseURL) + if err != nil || url.Scheme == "" || url.Host == "" { + return runnerErrors.NewBadRequestError("invalid api_base_url") + } + switch url.Scheme { + case httpsScheme, httpScheme: + default: + return runnerErrors.NewBadRequestError("invalid api_base_url") + } + + switch url.Scheme { + case httpsScheme, httpScheme: + default: + return runnerErrors.NewBadRequestError("invalid api_base_url") + } + + if c.BaseURL == "" { + return runnerErrors.NewBadRequestError("missing base_url") + } + + url, err = url.Parse(c.BaseURL) + if err != nil || url.Scheme == "" || url.Host == "" { + return runnerErrors.NewBadRequestError("invalid base_url") + } + + switch url.Scheme { + case httpsScheme, httpScheme: + default: + return runnerErrors.NewBadRequestError("invalid api_base_url") + } + + if c.CACertBundle != nil { + block, _ := pem.Decode(c.CACertBundle) + if block == nil { + return runnerErrors.NewBadRequestError("invalid ca_cert_bundle") + } + if _, err := x509.ParseCertificates(block.Bytes); err != nil { + return runnerErrors.NewBadRequestError("invalid ca_cert_bundle") + } + } + + return nil +} + +type UpdateGiteaEndpointParams struct { + Description *string `json:"description,omitempty"` + APIBaseURL *string `json:"api_base_url,omitempty"` + BaseURL *string `json:"base_url,omitempty"` + CACertBundle []byte `json:"ca_cert_bundle,omitempty"` +} + +func (u UpdateGiteaEndpointParams) Validate() error { + if u.APIBaseURL != nil { + url, err := url.Parse(*u.APIBaseURL) + if err != nil || url.Scheme == "" || url.Host == "" { + return runnerErrors.NewBadRequestError("invalid api_base_url") + } + switch url.Scheme { + case httpsScheme, httpScheme: + default: + return runnerErrors.NewBadRequestError("invalid api_base_url") + } + } + + if u.BaseURL != nil { + url, err := url.Parse(*u.BaseURL) + if err != nil || url.Scheme == "" || url.Host == "" { + return runnerErrors.NewBadRequestError("invalid base_url") + } + switch url.Scheme { + case httpsScheme, httpScheme: + default: + return runnerErrors.NewBadRequestError("invalid api_base_url") + } + } + + if u.CACertBundle != nil { + block, _ := pem.Decode(u.CACertBundle) + if block == nil { + return runnerErrors.NewBadRequestError("invalid ca_cert_bundle") + } + if _, err := x509.ParseCertificates(block.Bytes); err != nil { + return runnerErrors.NewBadRequestError("invalid ca_cert_bundle") + } + } + + return nil +} + +type CreateGiteaCredentialsParams struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Endpoint string `json:"endpoint,omitempty"` + AuthType ForgeAuthType `json:"auth_type,omitempty"` + PAT GithubPAT `json:"pat,omitempty"` + App GithubApp `json:"app,omitempty"` +} + +func (c CreateGiteaCredentialsParams) Validate() error { + if c.Name == "" { + return runnerErrors.NewBadRequestError("missing name") + } + + if c.Endpoint == "" { + return runnerErrors.NewBadRequestError("missing endpoint") + } + + switch c.AuthType { + case ForgeAuthTypePAT: + default: + return runnerErrors.NewBadRequestError("invalid auth_type: %s", c.AuthType) + } + + if c.AuthType == ForgeAuthTypePAT { + if c.PAT.OAuth2Token == "" { + return runnerErrors.NewBadRequestError("missing oauth2_token") + } + } + + return nil +} + +type UpdateGiteaCredentialsParams struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + PAT *GithubPAT `json:"pat,omitempty"` +} + +func (u UpdateGiteaCredentialsParams) Validate() error { + if u.PAT != nil { + if u.PAT.OAuth2Token == "" { + return runnerErrors.NewBadRequestError("missing oauth2_token") + } + } + + return nil +} diff --git a/runner/common/mocks/GithubClient.go b/runner/common/mocks/GithubClient.go index f1009d5af..36ef10796 100644 --- a/runner/common/mocks/GithubClient.go +++ b/runner/common/mocks/GithubClient.go @@ -118,18 +118,18 @@ func (_m *GithubClient) DeleteEntityHook(ctx context.Context, id int64) (*github } // GetEntity provides a mock function with no fields -func (_m *GithubClient) GetEntity() params.GithubEntity { +func (_m *GithubClient) GetEntity() params.ForgeEntity { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for GetEntity") } - var r0 params.GithubEntity - if rf, ok := ret.Get(0).(func() params.GithubEntity); ok { + var r0 params.ForgeEntity + if rf, ok := ret.Get(0).(func() params.ForgeEntity); ok { r0 = rf() } else { - r0 = ret.Get(0).(params.GithubEntity) + r0 = ret.Get(0).(params.ForgeEntity) } return r0 @@ -342,7 +342,7 @@ func (_m *GithubClient) ListEntityRunnerApplicationDownloads(ctx context.Context } // ListEntityRunners provides a mock function with given fields: ctx, opts -func (_m *GithubClient) ListEntityRunners(ctx context.Context, opts *github.ListOptions) (*github.Runners, *github.Response, error) { +func (_m *GithubClient) ListEntityRunners(ctx context.Context, opts *github.ListRunnersOptions) (*github.Runners, *github.Response, error) { ret := _m.Called(ctx, opts) if len(ret) == 0 { @@ -352,10 +352,10 @@ func (_m *GithubClient) ListEntityRunners(ctx context.Context, opts *github.List var r0 *github.Runners var r1 *github.Response var r2 error - if rf, ok := ret.Get(0).(func(context.Context, *github.ListOptions) (*github.Runners, *github.Response, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, *github.ListRunnersOptions) (*github.Runners, *github.Response, error)); ok { return rf(ctx, opts) } - if rf, ok := ret.Get(0).(func(context.Context, *github.ListOptions) *github.Runners); ok { + if rf, ok := ret.Get(0).(func(context.Context, *github.ListRunnersOptions) *github.Runners); ok { r0 = rf(ctx, opts) } else { if ret.Get(0) != nil { @@ -363,7 +363,7 @@ func (_m *GithubClient) ListEntityRunners(ctx context.Context, opts *github.List } } - if rf, ok := ret.Get(1).(func(context.Context, *github.ListOptions) *github.Response); ok { + if rf, ok := ret.Get(1).(func(context.Context, *github.ListRunnersOptions) *github.Response); ok { r1 = rf(ctx, opts) } else { if ret.Get(1) != nil { @@ -371,7 +371,7 @@ func (_m *GithubClient) ListEntityRunners(ctx context.Context, opts *github.List } } - if rf, ok := ret.Get(2).(func(context.Context, *github.ListOptions) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *github.ListRunnersOptions) error); ok { r2 = rf(ctx, opts) } else { r2 = ret.Error(2) @@ -410,6 +410,36 @@ func (_m *GithubClient) PingEntityHook(ctx context.Context, id int64) (*github.R return r0, r1 } +// RateLimit provides a mock function with given fields: ctx +func (_m *GithubClient) RateLimit(ctx context.Context) (*github.RateLimits, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for RateLimit") + } + + var r0 *github.RateLimits + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*github.RateLimits, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *github.RateLimits); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*github.RateLimits) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // RemoveEntityRunner provides a mock function with given fields: ctx, runnerID func (_m *GithubClient) RemoveEntityRunner(ctx context.Context, runnerID int64) error { ret := _m.Called(ctx, runnerID) diff --git a/runner/common/mocks/GithubEntityOperations.go b/runner/common/mocks/GithubEntityOperations.go index a482a9857..0aab99433 100644 --- a/runner/common/mocks/GithubEntityOperations.go +++ b/runner/common/mocks/GithubEntityOperations.go @@ -118,18 +118,18 @@ func (_m *GithubEntityOperations) DeleteEntityHook(ctx context.Context, id int64 } // GetEntity provides a mock function with no fields -func (_m *GithubEntityOperations) GetEntity() params.GithubEntity { +func (_m *GithubEntityOperations) GetEntity() params.ForgeEntity { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for GetEntity") } - var r0 params.GithubEntity - if rf, ok := ret.Get(0).(func() params.GithubEntity); ok { + var r0 params.ForgeEntity + if rf, ok := ret.Get(0).(func() params.ForgeEntity); ok { r0 = rf() } else { - r0 = ret.Get(0).(params.GithubEntity) + r0 = ret.Get(0).(params.ForgeEntity) } return r0 @@ -303,7 +303,7 @@ func (_m *GithubEntityOperations) ListEntityRunnerApplicationDownloads(ctx conte } // ListEntityRunners provides a mock function with given fields: ctx, opts -func (_m *GithubEntityOperations) ListEntityRunners(ctx context.Context, opts *github.ListOptions) (*github.Runners, *github.Response, error) { +func (_m *GithubEntityOperations) ListEntityRunners(ctx context.Context, opts *github.ListRunnersOptions) (*github.Runners, *github.Response, error) { ret := _m.Called(ctx, opts) if len(ret) == 0 { @@ -313,10 +313,10 @@ func (_m *GithubEntityOperations) ListEntityRunners(ctx context.Context, opts *g var r0 *github.Runners var r1 *github.Response var r2 error - if rf, ok := ret.Get(0).(func(context.Context, *github.ListOptions) (*github.Runners, *github.Response, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, *github.ListRunnersOptions) (*github.Runners, *github.Response, error)); ok { return rf(ctx, opts) } - if rf, ok := ret.Get(0).(func(context.Context, *github.ListOptions) *github.Runners); ok { + if rf, ok := ret.Get(0).(func(context.Context, *github.ListRunnersOptions) *github.Runners); ok { r0 = rf(ctx, opts) } else { if ret.Get(0) != nil { @@ -324,7 +324,7 @@ func (_m *GithubEntityOperations) ListEntityRunners(ctx context.Context, opts *g } } - if rf, ok := ret.Get(1).(func(context.Context, *github.ListOptions) *github.Response); ok { + if rf, ok := ret.Get(1).(func(context.Context, *github.ListRunnersOptions) *github.Response); ok { r1 = rf(ctx, opts) } else { if ret.Get(1) != nil { @@ -332,7 +332,7 @@ func (_m *GithubEntityOperations) ListEntityRunners(ctx context.Context, opts *g } } - if rf, ok := ret.Get(2).(func(context.Context, *github.ListOptions) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *github.ListRunnersOptions) error); ok { r2 = rf(ctx, opts) } else { r2 = ret.Error(2) @@ -371,6 +371,36 @@ func (_m *GithubEntityOperations) PingEntityHook(ctx context.Context, id int64) return r0, r1 } +// RateLimit provides a mock function with given fields: ctx +func (_m *GithubEntityOperations) RateLimit(ctx context.Context) (*github.RateLimits, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for RateLimit") + } + + var r0 *github.RateLimits + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*github.RateLimits, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *github.RateLimits); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*github.RateLimits) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // RemoveEntityRunner provides a mock function with given fields: ctx, runnerID func (_m *GithubEntityOperations) RemoveEntityRunner(ctx context.Context, runnerID int64) error { ret := _m.Called(ctx, runnerID) diff --git a/runner/common/mocks/RateLimitClient.go b/runner/common/mocks/RateLimitClient.go new file mode 100644 index 000000000..2c360217c --- /dev/null +++ b/runner/common/mocks/RateLimitClient.go @@ -0,0 +1,59 @@ +// Code generated by mockery v2.53.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + + github "github.com/google/go-github/v71/github" + mock "github.com/stretchr/testify/mock" +) + +// RateLimitClient is an autogenerated mock type for the RateLimitClient type +type RateLimitClient struct { + mock.Mock +} + +// RateLimit provides a mock function with given fields: ctx +func (_m *RateLimitClient) RateLimit(ctx context.Context) (*github.RateLimits, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for RateLimit") + } + + var r0 *github.RateLimits + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*github.RateLimits, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *github.RateLimits); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*github.RateLimits) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewRateLimitClient creates a new instance of RateLimitClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewRateLimitClient(t interface { + mock.TestingT + Cleanup(func()) +}) *RateLimitClient { + mock := &RateLimitClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/runner/common/util.go b/runner/common/util.go index 55e8fb00e..2720c4961 100644 --- a/runner/common/util.go +++ b/runner/common/util.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package common import ( @@ -23,7 +37,7 @@ type GithubEntityOperations interface { GetEntityJITConfig(ctx context.Context, instance string, pool params.Pool, labels []string) (jitConfigMap map[string]string, runner *github.Runner, err error) // GetEntity returns the GitHub entity for which the github client was instanciated. - GetEntity() params.GithubEntity + GetEntity() params.ForgeEntity // GithubBaseURL returns the base URL for the github or GHES API. GithubBaseURL() *url.URL } diff --git a/runner/common_test.go b/runner/common_test.go index b9b535452..247b5ab1f 100644 --- a/runner/common_test.go +++ b/runner/common_test.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package runner const ( diff --git a/runner/enterprises.go b/runner/enterprises.go index fb3f528b5..f192c7cdf 100644 --- a/runner/enterprises.go +++ b/runner/enterprises.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package runner import ( @@ -39,7 +53,7 @@ func (r *Runner) CreateEnterprise(ctx context.Context, param params.CreateEnterp return params.Enterprise{}, runnerErrors.NewConflictError("enterprise %s already exists", param.Name) } - enterprise, err = r.store.CreateEnterprise(ctx, param.Name, creds.Name, param.WebhookSecret, param.PoolBalancerType) + enterprise, err = r.store.CreateEnterprise(ctx, param.Name, creds, param.WebhookSecret, param.PoolBalancerType) if err != nil { return params.Enterprise{}, errors.Wrap(err, "creating enterprise") } @@ -206,9 +220,9 @@ func (r *Runner) CreateEnterprisePool(ctx context.Context, enterpriseID string, param.RunnerBootstrapTimeout = appdefaults.DefaultRunnerBootstrapTimeout } - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: enterpriseID, - EntityType: params.GithubEntityTypeEnterprise, + EntityType: params.ForgeEntityTypeEnterprise, } pool, err := r.store.CreateEntityPool(ctx, entity, createPoolParams) @@ -223,9 +237,9 @@ func (r *Runner) GetEnterprisePoolByID(ctx context.Context, enterpriseID, poolID if !auth.IsAdmin(ctx) { return params.Pool{}, runnerErrors.ErrUnauthorized } - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: enterpriseID, - EntityType: params.GithubEntityTypeEnterprise, + EntityType: params.ForgeEntityTypeEnterprise, } pool, err := r.store.GetEntityPool(ctx, entity, poolID) if err != nil { @@ -239,9 +253,9 @@ func (r *Runner) DeleteEnterprisePool(ctx context.Context, enterpriseID, poolID return runnerErrors.ErrUnauthorized } - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: enterpriseID, - EntityType: params.GithubEntityTypeEnterprise, + EntityType: params.ForgeEntityTypeEnterprise, } pool, err := r.store.GetEntityPool(ctx, entity, poolID) @@ -270,9 +284,9 @@ func (r *Runner) ListEnterprisePools(ctx context.Context, enterpriseID string) ( return []params.Pool{}, runnerErrors.ErrUnauthorized } - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: enterpriseID, - EntityType: params.GithubEntityTypeEnterprise, + EntityType: params.ForgeEntityTypeEnterprise, } pools, err := r.store.ListEntityPools(ctx, entity) if err != nil { @@ -286,9 +300,9 @@ func (r *Runner) UpdateEnterprisePool(ctx context.Context, enterpriseID, poolID return params.Pool{}, runnerErrors.ErrUnauthorized } - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: enterpriseID, - EntityType: params.GithubEntityTypeEnterprise, + EntityType: params.ForgeEntityTypeEnterprise, } pool, err := r.store.GetEntityPool(ctx, entity, poolID) if err != nil { @@ -320,9 +334,9 @@ func (r *Runner) ListEnterpriseInstances(ctx context.Context, enterpriseID strin if !auth.IsAdmin(ctx) { return nil, runnerErrors.ErrUnauthorized } - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: enterpriseID, - EntityType: params.GithubEntityTypeEnterprise, + EntityType: params.ForgeEntityTypeEnterprise, } instances, err := r.store.ListEntityInstances(ctx, entity) if err != nil { diff --git a/runner/enterprises_test.go b/runner/enterprises_test.go index 94bc48077..d5eef4632 100644 --- a/runner/enterprises_test.go +++ b/runner/enterprises_test.go @@ -39,7 +39,7 @@ type EnterpriseTestFixtures struct { Store dbCommon.Store StoreEnterprises map[string]params.Enterprise Providers map[string]common.Provider - Credentials map[string]params.GithubCredentials + Credentials map[string]params.ForgeCredentials CreateEnterpriseParams params.CreateEnterpriseParams CreatePoolParams params.CreatePoolParams CreateInstanceParams params.CreateInstanceParams @@ -56,9 +56,9 @@ type EnterpriseTestSuite struct { Fixtures *EnterpriseTestFixtures Runner *Runner - testCreds params.GithubCredentials - secondaryTestCreds params.GithubCredentials - githubEndpoint params.GithubEndpoint + testCreds params.ForgeCredentials + secondaryTestCreds params.ForgeCredentials + forgeEndpoint params.ForgeEndpoint } func (s *EnterpriseTestSuite) SetupTest() { @@ -70,9 +70,9 @@ func (s *EnterpriseTestSuite) SetupTest() { } adminCtx := garmTesting.ImpersonateAdminContext(context.Background(), db, s.T()) - s.githubEndpoint = garmTesting.CreateDefaultGithubEndpoint(adminCtx, db, s.T()) - s.testCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), s.githubEndpoint) - s.secondaryTestCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "secondary-creds", db, s.T(), s.githubEndpoint) + s.forgeEndpoint = garmTesting.CreateDefaultGithubEndpoint(adminCtx, db, s.T()) + s.testCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), s.forgeEndpoint) + s.secondaryTestCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "secondary-creds", db, s.T(), s.forgeEndpoint) // create some organization objects in the database, for testing purposes enterprises := map[string]params.Enterprise{} @@ -81,7 +81,7 @@ func (s *EnterpriseTestSuite) SetupTest() { enterprise, err := db.CreateEnterprise( adminCtx, name, - s.testCreds.Name, + s.testCreds, fmt.Sprintf("test-webhook-secret-%v", i), params.PoolBalancerTypeRoundRobin, ) @@ -103,7 +103,7 @@ func (s *EnterpriseTestSuite) SetupTest() { Providers: map[string]common.Provider{ "test-provider": providerMock, }, - Credentials: map[string]params.GithubCredentials{ + Credentials: map[string]params.ForgeCredentials{ s.testCreds.Name: s.testCreds, s.secondaryTestCreds.Name: s.secondaryTestCreds, }, @@ -270,9 +270,9 @@ func (s *EnterpriseTestSuite) TestDeleteEnterpriseErrUnauthorized() { } func (s *EnterpriseTestSuite) TestDeleteEnterprisePoolDefinedFailed() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, - EntityType: params.GithubEntityTypeEnterprise, + EntityType: params.ForgeEntityTypeEnterprise, } pool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { @@ -377,9 +377,9 @@ func (s *EnterpriseTestSuite) TestCreateEnterprisePoolFetchPoolParamsFailed() { } func (s *EnterpriseTestSuite) TestGetEnterprisePoolByID() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, - EntityType: params.GithubEntityTypeEnterprise, + EntityType: params.ForgeEntityTypeEnterprise, } enterprisePool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { @@ -399,9 +399,9 @@ func (s *EnterpriseTestSuite) TestGetEnterprisePoolByIDErrUnauthorized() { } func (s *EnterpriseTestSuite) TestDeleteEnterprisePool() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, - EntityType: params.GithubEntityTypeEnterprise, + EntityType: params.ForgeEntityTypeEnterprise, } pool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { @@ -423,9 +423,9 @@ func (s *EnterpriseTestSuite) TestDeleteEnterprisePoolErrUnauthorized() { } func (s *EnterpriseTestSuite) TestDeleteEnterprisePoolRunnersFailed() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, - EntityType: params.GithubEntityTypeEnterprise, + EntityType: params.ForgeEntityTypeEnterprise, } pool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { @@ -442,9 +442,9 @@ func (s *EnterpriseTestSuite) TestDeleteEnterprisePoolRunnersFailed() { } func (s *EnterpriseTestSuite) TestListEnterprisePools() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, - EntityType: params.GithubEntityTypeEnterprise, + EntityType: params.ForgeEntityTypeEnterprise, } enterprisePools := []params.Pool{} for i := 1; i <= 2; i++ { @@ -469,9 +469,9 @@ func (s *EnterpriseTestSuite) TestListOrgPoolsErrUnauthorized() { } func (s *EnterpriseTestSuite) TestUpdateEnterprisePool() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, - EntityType: params.GithubEntityTypeEnterprise, + EntityType: params.ForgeEntityTypeEnterprise, } enterprisePool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { @@ -492,9 +492,9 @@ func (s *EnterpriseTestSuite) TestUpdateEnterprisePoolErrUnauthorized() { } func (s *EnterpriseTestSuite) TestUpdateEnterprisePoolMinIdleGreaterThanMax() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, - EntityType: params.GithubEntityTypeEnterprise, + EntityType: params.ForgeEntityTypeEnterprise, } pool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { @@ -511,9 +511,9 @@ func (s *EnterpriseTestSuite) TestUpdateEnterprisePoolMinIdleGreaterThanMax() { } func (s *EnterpriseTestSuite) TestListEnterpriseInstances() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, - EntityType: params.GithubEntityTypeEnterprise, + EntityType: params.ForgeEntityTypeEnterprise, } pool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { diff --git a/runner/gitea_credentials.go b/runner/gitea_credentials.go new file mode 100644 index 000000000..4fdad1d2b --- /dev/null +++ b/runner/gitea_credentials.go @@ -0,0 +1,100 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package runner + +import ( + "context" + + "github.com/pkg/errors" + + runnerErrors "github.com/cloudbase/garm-provider-common/errors" + "github.com/cloudbase/garm/auth" + "github.com/cloudbase/garm/params" +) + +func (r *Runner) ListGiteaCredentials(ctx context.Context) ([]params.ForgeCredentials, error) { + if !auth.IsAdmin(ctx) { + return nil, runnerErrors.ErrUnauthorized + } + + // Get the credentials from the store. The cache is always updated after the database successfully + // commits the transaction that created/updated the credentials. + // If we create a set of credentials then immediately after we call ListGiteaCredentials, + // there is a posibillity that not all creds will be in the cache. + creds, err := r.store.ListGiteaCredentials(ctx) + if err != nil { + return nil, errors.Wrap(err, "fetching gitea credentials") + } + return creds, nil +} + +func (r *Runner) CreateGiteaCredentials(ctx context.Context, param params.CreateGiteaCredentialsParams) (params.ForgeCredentials, error) { + if !auth.IsAdmin(ctx) { + return params.ForgeCredentials{}, runnerErrors.ErrUnauthorized + } + + if err := param.Validate(); err != nil { + return params.ForgeCredentials{}, errors.Wrap(err, "failed to validate gitea credentials params") + } + + creds, err := r.store.CreateGiteaCredentials(ctx, param) + if err != nil { + return params.ForgeCredentials{}, errors.Wrap(err, "failed to create gitea credentials") + } + + return creds, nil +} + +func (r *Runner) GetGiteaCredentials(ctx context.Context, id uint) (params.ForgeCredentials, error) { + if !auth.IsAdmin(ctx) { + return params.ForgeCredentials{}, runnerErrors.ErrUnauthorized + } + + creds, err := r.store.GetGiteaCredentials(ctx, id, true) + if err != nil { + return params.ForgeCredentials{}, errors.Wrap(err, "failed to get gitea credentials") + } + + return creds, nil +} + +func (r *Runner) DeleteGiteaCredentials(ctx context.Context, id uint) error { + if !auth.IsAdmin(ctx) { + return runnerErrors.ErrUnauthorized + } + + if err := r.store.DeleteGiteaCredentials(ctx, id); err != nil { + return errors.Wrap(err, "failed to delete gitea credentials") + } + + return nil +} + +func (r *Runner) UpdateGiteaCredentials(ctx context.Context, id uint, param params.UpdateGiteaCredentialsParams) (params.ForgeCredentials, error) { + if !auth.IsAdmin(ctx) { + return params.ForgeCredentials{}, runnerErrors.ErrUnauthorized + } + + if err := param.Validate(); err != nil { + return params.ForgeCredentials{}, errors.Wrap(err, "failed to validate gitea credentials params") + } + + newCreds, err := r.store.UpdateGiteaCredentials(ctx, id, param) + if err != nil { + return params.ForgeCredentials{}, errors.Wrap(err, "failed to update gitea credentials") + } + + return newCreds, nil +} diff --git a/runner/gitea_endpoints.go b/runner/gitea_endpoints.go new file mode 100644 index 000000000..181f8e7ef --- /dev/null +++ b/runner/gitea_endpoints.go @@ -0,0 +1,96 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package runner + +import ( + "context" + + "github.com/pkg/errors" + + runnerErrors "github.com/cloudbase/garm-provider-common/errors" + "github.com/cloudbase/garm/auth" + "github.com/cloudbase/garm/params" +) + +func (r *Runner) CreateGiteaEndpoint(ctx context.Context, param params.CreateGiteaEndpointParams) (params.ForgeEndpoint, error) { + if !auth.IsAdmin(ctx) { + return params.ForgeEndpoint{}, runnerErrors.ErrUnauthorized + } + + if err := param.Validate(); err != nil { + return params.ForgeEndpoint{}, errors.Wrap(err, "failed to validate gitea endpoint params") + } + + ep, err := r.store.CreateGiteaEndpoint(ctx, param) + if err != nil { + return params.ForgeEndpoint{}, errors.Wrap(err, "failed to create gitea endpoint") + } + + return ep, nil +} + +func (r *Runner) GetGiteaEndpoint(ctx context.Context, name string) (params.ForgeEndpoint, error) { + if !auth.IsAdmin(ctx) { + return params.ForgeEndpoint{}, runnerErrors.ErrUnauthorized + } + endpoint, err := r.store.GetGiteaEndpoint(ctx, name) + if err != nil { + return params.ForgeEndpoint{}, errors.Wrap(err, "failed to get gitea endpoint") + } + + return endpoint, nil +} + +func (r *Runner) DeleteGiteaEndpoint(ctx context.Context, name string) error { + if !auth.IsAdmin(ctx) { + return runnerErrors.ErrUnauthorized + } + + err := r.store.DeleteGiteaEndpoint(ctx, name) + if err != nil { + return errors.Wrap(err, "failed to delete gitea endpoint") + } + + return nil +} + +func (r *Runner) UpdateGiteaEndpoint(ctx context.Context, name string, param params.UpdateGiteaEndpointParams) (params.ForgeEndpoint, error) { + if !auth.IsAdmin(ctx) { + return params.ForgeEndpoint{}, runnerErrors.ErrUnauthorized + } + + if err := param.Validate(); err != nil { + return params.ForgeEndpoint{}, errors.Wrap(err, "failed to validate gitea endpoint params") + } + + newEp, err := r.store.UpdateGiteaEndpoint(ctx, name, param) + if err != nil { + return params.ForgeEndpoint{}, errors.Wrap(err, "failed to update gitea endpoint") + } + return newEp, nil +} + +func (r *Runner) ListGiteaEndpoints(ctx context.Context) ([]params.ForgeEndpoint, error) { + if !auth.IsAdmin(ctx) { + return nil, runnerErrors.ErrUnauthorized + } + + endpoints, err := r.store.ListGiteaEndpoints(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to list gitea endpoints") + } + + return endpoints, nil +} diff --git a/runner/github_credentials.go b/runner/github_credentials.go index 7cd4e74cf..ec524056a 100644 --- a/runner/github_credentials.go +++ b/runner/github_credentials.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package runner import ( @@ -11,7 +25,7 @@ import ( "github.com/cloudbase/garm/params" ) -func (r *Runner) ListCredentials(ctx context.Context) ([]params.GithubCredentials, error) { +func (r *Runner) ListCredentials(ctx context.Context) ([]params.ForgeCredentials, error) { if !auth.IsAdmin(ctx) { return nil, runnerErrors.ErrUnauthorized } @@ -37,31 +51,31 @@ func (r *Runner) ListCredentials(ctx context.Context) ([]params.GithubCredential return creds, nil } -func (r *Runner) CreateGithubCredentials(ctx context.Context, param params.CreateGithubCredentialsParams) (params.GithubCredentials, error) { +func (r *Runner) CreateGithubCredentials(ctx context.Context, param params.CreateGithubCredentialsParams) (params.ForgeCredentials, error) { if !auth.IsAdmin(ctx) { - return params.GithubCredentials{}, runnerErrors.ErrUnauthorized + return params.ForgeCredentials{}, runnerErrors.ErrUnauthorized } if err := param.Validate(); err != nil { - return params.GithubCredentials{}, errors.Wrap(err, "failed to validate github credentials params") + return params.ForgeCredentials{}, errors.Wrap(err, "failed to validate github credentials params") } creds, err := r.store.CreateGithubCredentials(ctx, param) if err != nil { - return params.GithubCredentials{}, errors.Wrap(err, "failed to create github credentials") + return params.ForgeCredentials{}, errors.Wrap(err, "failed to create github credentials") } return creds, nil } -func (r *Runner) GetGithubCredentials(ctx context.Context, id uint) (params.GithubCredentials, error) { +func (r *Runner) GetGithubCredentials(ctx context.Context, id uint) (params.ForgeCredentials, error) { if !auth.IsAdmin(ctx) { - return params.GithubCredentials{}, runnerErrors.ErrUnauthorized + return params.ForgeCredentials{}, runnerErrors.ErrUnauthorized } creds, err := r.store.GetGithubCredentials(ctx, id, true) if err != nil { - return params.GithubCredentials{}, errors.Wrap(err, "failed to get github credentials") + return params.ForgeCredentials{}, errors.Wrap(err, "failed to get github credentials") } cached, ok := cache.GetGithubCredentials((creds.ID)) @@ -84,18 +98,18 @@ func (r *Runner) DeleteGithubCredentials(ctx context.Context, id uint) error { return nil } -func (r *Runner) UpdateGithubCredentials(ctx context.Context, id uint, param params.UpdateGithubCredentialsParams) (params.GithubCredentials, error) { +func (r *Runner) UpdateGithubCredentials(ctx context.Context, id uint, param params.UpdateGithubCredentialsParams) (params.ForgeCredentials, error) { if !auth.IsAdmin(ctx) { - return params.GithubCredentials{}, runnerErrors.ErrUnauthorized + return params.ForgeCredentials{}, runnerErrors.ErrUnauthorized } if err := param.Validate(); err != nil { - return params.GithubCredentials{}, errors.Wrap(err, "failed to validate github credentials params") + return params.ForgeCredentials{}, errors.Wrap(err, "failed to validate github credentials params") } newCreds, err := r.store.UpdateGithubCredentials(ctx, id, param) if err != nil { - return params.GithubCredentials{}, errors.Wrap(err, "failed to update github credentials") + return params.ForgeCredentials{}, errors.Wrap(err, "failed to update github credentials") } return newCreds, nil diff --git a/runner/github_endpoints.go b/runner/github_endpoints.go index 1f6431eaf..0e144447d 100644 --- a/runner/github_endpoints.go +++ b/runner/github_endpoints.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package runner import ( @@ -10,30 +24,30 @@ import ( "github.com/cloudbase/garm/params" ) -func (r *Runner) CreateGithubEndpoint(ctx context.Context, param params.CreateGithubEndpointParams) (params.GithubEndpoint, error) { +func (r *Runner) CreateGithubEndpoint(ctx context.Context, param params.CreateGithubEndpointParams) (params.ForgeEndpoint, error) { if !auth.IsAdmin(ctx) { - return params.GithubEndpoint{}, runnerErrors.ErrUnauthorized + return params.ForgeEndpoint{}, runnerErrors.ErrUnauthorized } if err := param.Validate(); err != nil { - return params.GithubEndpoint{}, errors.Wrap(err, "failed to validate github endpoint params") + return params.ForgeEndpoint{}, errors.Wrap(err, "failed to validate github endpoint params") } ep, err := r.store.CreateGithubEndpoint(ctx, param) if err != nil { - return params.GithubEndpoint{}, errors.Wrap(err, "failed to create github endpoint") + return params.ForgeEndpoint{}, errors.Wrap(err, "failed to create github endpoint") } return ep, nil } -func (r *Runner) GetGithubEndpoint(ctx context.Context, name string) (params.GithubEndpoint, error) { +func (r *Runner) GetGithubEndpoint(ctx context.Context, name string) (params.ForgeEndpoint, error) { if !auth.IsAdmin(ctx) { - return params.GithubEndpoint{}, runnerErrors.ErrUnauthorized + return params.ForgeEndpoint{}, runnerErrors.ErrUnauthorized } endpoint, err := r.store.GetGithubEndpoint(ctx, name) if err != nil { - return params.GithubEndpoint{}, errors.Wrap(err, "failed to get github endpoint") + return params.ForgeEndpoint{}, errors.Wrap(err, "failed to get github endpoint") } return endpoint, nil @@ -52,23 +66,23 @@ func (r *Runner) DeleteGithubEndpoint(ctx context.Context, name string) error { return nil } -func (r *Runner) UpdateGithubEndpoint(ctx context.Context, name string, param params.UpdateGithubEndpointParams) (params.GithubEndpoint, error) { +func (r *Runner) UpdateGithubEndpoint(ctx context.Context, name string, param params.UpdateGithubEndpointParams) (params.ForgeEndpoint, error) { if !auth.IsAdmin(ctx) { - return params.GithubEndpoint{}, runnerErrors.ErrUnauthorized + return params.ForgeEndpoint{}, runnerErrors.ErrUnauthorized } if err := param.Validate(); err != nil { - return params.GithubEndpoint{}, errors.Wrap(err, "failed to validate github endpoint params") + return params.ForgeEndpoint{}, errors.Wrap(err, "failed to validate github endpoint params") } newEp, err := r.store.UpdateGithubEndpoint(ctx, name, param) if err != nil { - return params.GithubEndpoint{}, errors.Wrap(err, "failed to update github endpoint") + return params.ForgeEndpoint{}, errors.Wrap(err, "failed to update github endpoint") } return newEp, nil } -func (r *Runner) ListGithubEndpoints(ctx context.Context) ([]params.GithubEndpoint, error) { +func (r *Runner) ListGithubEndpoints(ctx context.Context) ([]params.ForgeEndpoint, error) { if !auth.IsAdmin(ctx) { return nil, runnerErrors.ErrUnauthorized } diff --git a/runner/metadata.go b/runner/metadata.go index 3892d350b..2c917ea07 100644 --- a/runner/metadata.go +++ b/runner/metadata.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package runner import ( @@ -16,7 +30,7 @@ import ( "github.com/cloudbase/garm/params" ) -var systemdUnitTemplate = `[Unit] +var githubSystemdUnitTemplate = `[Unit] Description=GitHub Actions Runner ({{.ServiceName}}) After=network.target @@ -32,11 +46,24 @@ TimeoutStopSec=5min WantedBy=multi-user.target ` -func validateInstanceState(ctx context.Context) (params.Instance, error) { - if !auth.InstanceHasJITConfig(ctx) { - return params.Instance{}, fmt.Errorf("instance not configured for JIT: %w", runnerErrors.ErrNotFound) - } +var giteaSystemdUnitTemplate = `[Unit] +Description=Act Runner ({{.ServiceName}}) +After=network.target + +[Service] +ExecStart=/home/{{.RunAsUser}}/act-runner/act_runner daemon --once +User={{.RunAsUser}} +WorkingDirectory=/home/{{.RunAsUser}}/act-runner +KillMode=process +KillSignal=SIGTERM +TimeoutStopSec=5min +Restart=always + +[Install] +WantedBy=multi-user.target +` +func validateInstanceState(ctx context.Context) (params.Instance, error) { status := auth.InstanceRunnerStatus(ctx) if status != params.RunnerPending && status != params.RunnerInstalling { return params.Instance{}, runnerErrors.ErrUnauthorized @@ -49,6 +76,56 @@ func validateInstanceState(ctx context.Context) (params.Instance, error) { return instance, nil } +func (r *Runner) getForgeEntityFromInstance(ctx context.Context, instance params.Instance) (params.ForgeEntity, error) { + var entityGetter params.EntityGetter + var err error + switch { + case instance.PoolID != "": + entityGetter, err = r.store.GetPoolByID(r.ctx, instance.PoolID) + case instance.ScaleSetID != 0: + entityGetter, err = r.store.GetScaleSetByID(r.ctx, instance.ScaleSetID) + default: + return params.ForgeEntity{}, errors.New("instance not associated with a pool or scale set") + } + + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext( + ctx, "failed to get entity getter", + "instance", instance.Name) + return params.ForgeEntity{}, errors.Wrap(err, "fetching entity getter") + } + + poolEntity, err := entityGetter.GetEntity() + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext( + ctx, "failed to get entity", + "instance", instance.Name) + return params.ForgeEntity{}, errors.Wrap(err, "fetching entity") + } + + entity, err := r.store.GetForgeEntity(r.ctx, poolEntity.EntityType, poolEntity.ID) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext( + ctx, "failed to get entity", + "instance", instance.Name) + return params.ForgeEntity{}, errors.Wrap(err, "fetching entity") + } + return entity, nil +} + +func (r *Runner) getServiceNameForEntity(entity params.ForgeEntity) (string, error) { + switch entity.EntityType { + case params.ForgeEntityTypeEnterprise: + return fmt.Sprintf("actions.runner.%s.%s", entity.Owner, entity.Name), nil + case params.ForgeEntityTypeOrganization: + return fmt.Sprintf("actions.runner.%s.%s", entity.Owner, entity.Name), nil + case params.ForgeEntityTypeRepository: + return fmt.Sprintf("actions.runner.%s-%s.%s", entity.Owner, entity.Name, entity.Name), nil + default: + return "", errors.New("unknown entity type") + } +} + func (r *Runner) GetRunnerServiceName(ctx context.Context) (string, error) { instance, err := validateInstanceState(ctx) if err != nil { @@ -56,64 +133,51 @@ func (r *Runner) GetRunnerServiceName(ctx context.Context) (string, error) { ctx, "failed to get instance params") return "", runnerErrors.ErrUnauthorized } - var entity params.GithubEntity - - switch { - case instance.PoolID != "": - pool, err := r.store.GetPoolByID(r.ctx, instance.PoolID) - if err != nil { - slog.With(slog.Any("error", err)).ErrorContext( - ctx, "failed to get pool", - "pool_id", instance.PoolID) - return "", errors.Wrap(err, "fetching pool") - } - entity, err = pool.GetEntity() - if err != nil { - slog.With(slog.Any("error", err)).ErrorContext( - ctx, "failed to get pool entity", - "pool_id", instance.PoolID) - return "", errors.Wrap(err, "fetching pool entity") - } - case instance.ScaleSetID != 0: - scaleSet, err := r.store.GetScaleSetByID(r.ctx, instance.ScaleSetID) - if err != nil { - slog.With(slog.Any("error", err)).ErrorContext( - ctx, "failed to get scale set", - "scale_set_id", instance.ScaleSetID) - return "", errors.Wrap(err, "fetching scale set") - } - entity, err = scaleSet.GetEntity() - if err != nil { - slog.With(slog.Any("error", err)).ErrorContext( - ctx, "failed to get scale set entity", - "scale_set_id", instance.ScaleSetID) - return "", errors.Wrap(err, "fetching scale set entity") - } - default: - return "", errors.New("instance not associated with a pool or scale set") + entity, err := r.getForgeEntityFromInstance(ctx, instance) + if err != nil { + slog.ErrorContext(r.ctx, "failed to get entity", "error", err) + return "", errors.Wrap(err, "fetching entity") } - tpl := "actions.runner.%s.%s" - var serviceName string - switch entity.EntityType { - case params.GithubEntityTypeEnterprise: - serviceName = fmt.Sprintf(tpl, entity.Owner, instance.Name) - case params.GithubEntityTypeOrganization: - serviceName = fmt.Sprintf(tpl, entity.Owner, instance.Name) - case params.GithubEntityTypeRepository: - serviceName = fmt.Sprintf(tpl, fmt.Sprintf("%s-%s", entity.Owner, entity.Name), instance.Name) + serviceName, err := r.getServiceNameForEntity(entity) + if err != nil { + slog.ErrorContext(r.ctx, "failed to get service name", "error", err) + return "", errors.Wrap(err, "fetching service name") } return serviceName, nil } func (r *Runner) GenerateSystemdUnitFile(ctx context.Context, runAsUser string) ([]byte, error) { - serviceName, err := r.GetRunnerServiceName(ctx) + instance, err := validateInstanceState(ctx) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext( + ctx, "failed to get instance params") + return nil, runnerErrors.ErrUnauthorized + } + entity, err := r.getForgeEntityFromInstance(ctx, instance) if err != nil { - return nil, errors.Wrap(err, "fetching runner service name") + slog.ErrorContext(r.ctx, "failed to get entity", "error", err) + return nil, errors.Wrap(err, "fetching entity") } - unitTemplate, err := template.New("").Parse(systemdUnitTemplate) + serviceName, err := r.getServiceNameForEntity(entity) if err != nil { + slog.ErrorContext(r.ctx, "failed to get service name", "error", err) + return nil, errors.Wrap(err, "fetching service name") + } + + var unitTemplate *template.Template + switch entity.Credentials.ForgeType { + case params.GithubEndpointType: + unitTemplate, err = template.New("").Parse(githubSystemdUnitTemplate) + case params.GiteaEndpointType: + unitTemplate, err = template.New("").Parse(giteaSystemdUnitTemplate) + default: + slog.ErrorContext(r.ctx, "unknown forge type", "forge_type", entity.Credentials.ForgeType) + return nil, errors.New("unknown forge type") + } + if err != nil { + slog.ErrorContext(r.ctx, "failed to parse template", "error", err) return nil, errors.Wrap(err, "parsing template") } @@ -131,12 +195,17 @@ func (r *Runner) GenerateSystemdUnitFile(ctx context.Context, runAsUser string) var unitFile bytes.Buffer if err := unitTemplate.Execute(&unitFile, data); err != nil { + slog.ErrorContext(r.ctx, "failed to execute template", "error", err) return nil, errors.Wrap(err, "executing template") } return unitFile.Bytes(), nil } func (r *Runner) GetJITConfigFile(ctx context.Context, file string) ([]byte, error) { + if !auth.InstanceHasJITConfig(ctx) { + return nil, fmt.Errorf("instance not configured for JIT: %w", runnerErrors.ErrNotFound) + } + instance, err := validateInstanceState(ctx) if err != nil { slog.With(slog.Any("error", err)).ErrorContext( diff --git a/runner/metrics/enterprise.go b/runner/metrics/enterprise.go index 407c0fc40..3ab9003cf 100644 --- a/runner/metrics/enterprise.go +++ b/runner/metrics/enterprise.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package metrics import ( diff --git a/runner/metrics/health.go b/runner/metrics/health.go index 05e1ed9ba..fcd254dfb 100644 --- a/runner/metrics/health.go +++ b/runner/metrics/health.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package metrics import ( diff --git a/runner/metrics/instance.go b/runner/metrics/instance.go index 06fd48817..bc6bed0a6 100644 --- a/runner/metrics/instance.go +++ b/runner/metrics/instance.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package metrics import ( diff --git a/runner/metrics/metrics.go b/runner/metrics/metrics.go index f9f708644..772ba86a2 100644 --- a/runner/metrics/metrics.go +++ b/runner/metrics/metrics.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package metrics import ( diff --git a/runner/metrics/organization.go b/runner/metrics/organization.go index 6b9f6b717..3716cca13 100644 --- a/runner/metrics/organization.go +++ b/runner/metrics/organization.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package metrics import ( diff --git a/runner/metrics/pool.go b/runner/metrics/pool.go index 44ad27a85..6b06a8b9d 100644 --- a/runner/metrics/pool.go +++ b/runner/metrics/pool.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package metrics import ( diff --git a/runner/metrics/provider.go b/runner/metrics/provider.go index e2b38a9fe..1d7a065d1 100644 --- a/runner/metrics/provider.go +++ b/runner/metrics/provider.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package metrics import ( diff --git a/runner/metrics/repository.go b/runner/metrics/repository.go index b76fcc0ec..36e07bf0a 100644 --- a/runner/metrics/repository.go +++ b/runner/metrics/repository.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package metrics import ( diff --git a/runner/organizations.go b/runner/organizations.go index 4b5e3fd75..bddab87c6 100644 --- a/runner/organizations.go +++ b/runner/organizations.go @@ -38,7 +38,18 @@ func (r *Runner) CreateOrganization(ctx context.Context, param params.CreateOrgP return params.Organization{}, errors.Wrap(err, "validating params") } - creds, err := r.store.GetGithubCredentialsByName(ctx, param.CredentialsName, true) + var creds params.ForgeCredentials + switch param.GetForgeType() { + case params.GithubEndpointType: + slog.DebugContext(ctx, "getting github credentials") + creds, err = r.store.GetGithubCredentialsByName(ctx, param.CredentialsName, true) + case params.GiteaEndpointType: + slog.DebugContext(ctx, "getting gitea credentials") + creds, err = r.store.GetGiteaCredentialsByName(ctx, param.CredentialsName, true) + default: + return params.Organization{}, runnerErrors.NewBadRequestError("invalid forge type: %s", param.GetForgeType()) + } + if err != nil { return params.Organization{}, runnerErrors.NewBadRequestError("credentials %s not defined", param.CredentialsName) } @@ -52,7 +63,7 @@ func (r *Runner) CreateOrganization(ctx context.Context, param params.CreateOrgP return params.Organization{}, runnerErrors.NewConflictError("organization %s already exists", param.Name) } - org, err = r.store.CreateOrganization(ctx, param.Name, creds.Name, param.WebhookSecret, param.PoolBalancerType) + org, err = r.store.CreateOrganization(ctx, param.Name, creds, param.WebhookSecret, param.PoolBalancerType) if err != nil { return params.Organization{}, errors.Wrap(err, "creating organization") } @@ -235,9 +246,9 @@ func (r *Runner) CreateOrgPool(ctx context.Context, orgID string, param params.C param.RunnerBootstrapTimeout = appdefaults.DefaultRunnerBootstrapTimeout } - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: orgID, - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, } pool, err := r.store.CreateEntityPool(ctx, entity, createPoolParams) @@ -253,9 +264,9 @@ func (r *Runner) GetOrgPoolByID(ctx context.Context, orgID, poolID string) (para return params.Pool{}, runnerErrors.ErrUnauthorized } - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: orgID, - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, } pool, err := r.store.GetEntityPool(ctx, entity, poolID) @@ -271,9 +282,9 @@ func (r *Runner) DeleteOrgPool(ctx context.Context, orgID, poolID string) error return runnerErrors.ErrUnauthorized } - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: orgID, - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, } pool, err := r.store.GetEntityPool(ctx, entity, poolID) @@ -304,9 +315,9 @@ func (r *Runner) ListOrgPools(ctx context.Context, orgID string) ([]params.Pool, if !auth.IsAdmin(ctx) { return []params.Pool{}, runnerErrors.ErrUnauthorized } - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: orgID, - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, } pools, err := r.store.ListEntityPools(ctx, entity) if err != nil { @@ -320,9 +331,9 @@ func (r *Runner) UpdateOrgPool(ctx context.Context, orgID, poolID string, param return params.Pool{}, runnerErrors.ErrUnauthorized } - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: orgID, - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, } pool, err := r.store.GetEntityPool(ctx, entity, poolID) @@ -356,9 +367,9 @@ func (r *Runner) ListOrgInstances(ctx context.Context, orgID string) ([]params.I return nil, runnerErrors.ErrUnauthorized } - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: orgID, - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, } instances, err := r.store.ListEntityInstances(ctx, entity) diff --git a/runner/organizations_test.go b/runner/organizations_test.go index ae0af3cf4..90075c87f 100644 --- a/runner/organizations_test.go +++ b/runner/organizations_test.go @@ -39,7 +39,7 @@ type OrgTestFixtures struct { Store dbCommon.Store StoreOrgs map[string]params.Organization Providers map[string]common.Provider - Credentials map[string]params.GithubCredentials + Credentials map[string]params.ForgeCredentials CreateOrgParams params.CreateOrgParams CreatePoolParams params.CreatePoolParams CreateInstanceParams params.CreateInstanceParams @@ -56,9 +56,9 @@ type OrgTestSuite struct { Fixtures *OrgTestFixtures Runner *Runner - testCreds params.GithubCredentials - secondaryTestCreds params.GithubCredentials - githubEndpoint params.GithubEndpoint + testCreds params.ForgeCredentials + secondaryTestCreds params.ForgeCredentials + githubEndpoint params.ForgeEndpoint } func (s *OrgTestSuite) SetupTest() { @@ -82,7 +82,7 @@ func (s *OrgTestSuite) SetupTest() { org, err := db.CreateOrganization( adminCtx, name, - s.testCreds.Name, + s.testCreds, fmt.Sprintf("test-webhook-secret-%v", i), params.PoolBalancerTypeRoundRobin, ) @@ -104,7 +104,7 @@ func (s *OrgTestSuite) SetupTest() { Providers: map[string]common.Provider{ "test-provider": providerMock, }, - Credentials: map[string]params.GithubCredentials{ + Credentials: map[string]params.ForgeCredentials{ s.testCreds.Name: s.testCreds, s.secondaryTestCreds.Name: s.secondaryTestCreds, }, @@ -284,9 +284,9 @@ func (s *OrgTestSuite) TestDeleteOrganizationErrUnauthorized() { } func (s *OrgTestSuite) TestDeleteOrganizationPoolDefinedFailed() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreOrgs["test-org-1"].ID, - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, } pool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { @@ -402,9 +402,9 @@ func (s *OrgTestSuite) TestCreateOrgPoolFetchPoolParamsFailed() { } func (s *OrgTestSuite) TestGetOrgPoolByID() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreOrgs["test-org-1"].ID, - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, } orgPool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { @@ -424,9 +424,9 @@ func (s *OrgTestSuite) TestGetOrgPoolByIDErrUnauthorized() { } func (s *OrgTestSuite) TestDeleteOrgPool() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreOrgs["test-org-1"].ID, - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, } pool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { @@ -448,9 +448,9 @@ func (s *OrgTestSuite) TestDeleteOrgPoolErrUnauthorized() { } func (s *OrgTestSuite) TestDeleteOrgPoolRunnersFailed() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreOrgs["test-org-1"].ID, - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, } pool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { @@ -467,9 +467,9 @@ func (s *OrgTestSuite) TestDeleteOrgPoolRunnersFailed() { } func (s *OrgTestSuite) TestListOrgPools() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreOrgs["test-org-1"].ID, - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, } orgPools := []params.Pool{} for i := 1; i <= 2; i++ { @@ -494,9 +494,9 @@ func (s *OrgTestSuite) TestListOrgPoolsErrUnauthorized() { } func (s *OrgTestSuite) TestUpdateOrgPool() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreOrgs["test-org-1"].ID, - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, } orgPool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { @@ -517,9 +517,9 @@ func (s *OrgTestSuite) TestUpdateOrgPoolErrUnauthorized() { } func (s *OrgTestSuite) TestUpdateOrgPoolMinIdleGreaterThanMax() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreOrgs["test-org-1"].ID, - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, } pool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { @@ -536,9 +536,9 @@ func (s *OrgTestSuite) TestUpdateOrgPoolMinIdleGreaterThanMax() { } func (s *OrgTestSuite) TestListOrgInstances() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreOrgs["test-org-1"].ID, - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, } pool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { diff --git a/runner/pool/common.go b/runner/pool/common.go index 6820be1ae..5316e07e9 100644 --- a/runner/pool/common.go +++ b/runner/pool/common.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package pool import ( diff --git a/runner/pool/pool.go b/runner/pool/pool.go index 3cb8bff38..ca95867f1 100644 --- a/runner/pool/pool.go +++ b/runner/pool/pool.go @@ -47,15 +47,15 @@ import ( ) var ( - poolIDLabelprefix = "runner-pool-id:" - controllerLabelPrefix = "runner-controller-id:" + poolIDLabelprefix = "runner-pool-id" + controllerLabelPrefix = "runner-controller-id" // We tag runners that have been spawned as a result of a queued job with the job ID // that spawned them. There is no way to guarantee that the runner spawned in response to a particular // job, will be picked up by that job. We mark them so as in the very likely event that the runner // has picked up a different job, we can clear the lock on the job that spaned it. // The job it picked up would already be transitioned to in_progress so it will be ignored by the // consume loop. - jobLabelPrefix = "in_response_to_job:" + jobLabelPrefix = "in_response_to_job" ) const ( @@ -67,7 +67,7 @@ const ( maxCreateAttempts = 5 ) -func NewEntityPoolManager(ctx context.Context, entity params.GithubEntity, instanceTokenGetter auth.InstanceTokenGetter, providers map[string]common.Provider, store dbCommon.Store) (common.PoolManager, error) { +func NewEntityPoolManager(ctx context.Context, entity params.ForgeEntity, instanceTokenGetter auth.InstanceTokenGetter, providers map[string]common.Provider, store dbCommon.Store) (common.PoolManager, error) { ctx = garmUtil.WithSlogContext(ctx, slog.Any("pool_mgr", entity.String()), slog.Any("pool_type", entity.EntityType)) ghc, err := ghClient.Client(ctx, entity) if err != nil { @@ -120,7 +120,7 @@ func NewEntityPoolManager(ctx context.Context, entity params.GithubEntity, insta type basePoolManager struct { ctx context.Context consumerID string - entity params.GithubEntity + entity params.ForgeEntity ghcli common.GithubClient controllerInfo params.ControllerInfo instanceTokenGetter auth.InstanceTokenGetter @@ -152,6 +152,7 @@ func (r *basePoolManager) getProviderBaseParams(pool params.Pool) common.Provide func (r *basePoolManager) HandleWorkflowJob(job params.WorkflowJob) error { if err := r.ValidateOwner(job); err != nil { + slog.ErrorContext(r.ctx, "failed to validate owner", "error", err) return errors.Wrap(err, "validating owner") } @@ -164,6 +165,7 @@ func (r *basePoolManager) HandleWorkflowJob(job params.WorkflowJob) error { jobParams, err := r.paramsWorkflowJobToParamsJob(job) if err != nil { + slog.ErrorContext(r.ctx, "failed to convert job to params", "error", err) return errors.Wrap(err, "converting job to params") } @@ -296,7 +298,8 @@ func (r *basePoolManager) HandleWorkflowJob(job params.WorkflowJob) error { func jobIDFromLabels(labels []string) int64 { for _, lbl := range labels { if strings.HasPrefix(lbl, jobLabelPrefix) { - jobID, err := strconv.ParseInt(lbl[len(jobLabelPrefix):], 10, 64) + trimLength := min(len(jobLabelPrefix)+1, len(lbl)) + jobID, err := strconv.ParseInt(lbl[trimLength:], 10, 64) if err != nil { return 0 } @@ -361,21 +364,21 @@ func (r *basePoolManager) startLoopForFunction(f func() error, interval time.Dur } func (r *basePoolManager) updateTools() error { - // Update tools cache. - tools, err := r.FetchTools() + tools, err := cache.GetGithubToolsCache(r.entity.ID) if err != nil { slog.With(slog.Any("error", err)).ErrorContext( r.ctx, "failed to update tools for entity", "entity", r.entity.String()) r.SetPoolRunningState(false, err.Error()) return fmt.Errorf("failed to update tools for entity %s: %w", r.entity.String(), err) } + r.mux.Lock() r.tools = tools r.mux.Unlock() slog.DebugContext(r.ctx, "successfully updated tools") r.SetPoolRunningState(true, "") - return err + return nil } // cleanupOrphanedProviderRunners compares runners in github with local runners and removes @@ -866,8 +869,7 @@ func (r *basePoolManager) addInstanceToProvider(instance params.Instance) error jwtValidity := pool.RunnerTimeout() - entity := r.entity.String() - jwtToken, err := r.instanceTokenGetter.NewInstanceJWTToken(instance, entity, pool.PoolType(), jwtValidity) + jwtToken, err := r.instanceTokenGetter.NewInstanceJWTToken(instance, r.entity, pool.PoolType(), jwtValidity) if err != nil { return errors.Wrap(err, "fetching instance jwt token") } @@ -877,7 +879,7 @@ func (r *basePoolManager) addInstanceToProvider(instance params.Instance) error bootstrapArgs := commonParams.BootstrapInstance{ Name: instance.Name, Tools: r.tools, - RepoURL: r.entity.GithubURL(), + RepoURL: r.entity.ForgeURL(), MetadataURL: instance.MetadataURL, CallbackURL: instance.CallbackURL, InstanceToken: jwtToken, @@ -981,11 +983,11 @@ func (r *basePoolManager) paramsWorkflowJobToParamsJob(job params.WorkflowJob) ( } switch r.entity.EntityType { - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: jobParams.EnterpriseID = &asUUID - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: jobParams.RepoID = &asUUID - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: jobParams.OrgID = &asUUID default: return jobParams, errors.Errorf("unknown pool type: %s", r.entity.EntityType) @@ -995,11 +997,11 @@ func (r *basePoolManager) paramsWorkflowJobToParamsJob(job params.WorkflowJob) ( } func (r *basePoolManager) poolLabel(poolID string) string { - return fmt.Sprintf("%s%s", poolIDLabelprefix, poolID) + return fmt.Sprintf("%s=%s", poolIDLabelprefix, poolID) } func (r *basePoolManager) controllerLabel() string { - return fmt.Sprintf("%s%s", controllerLabelPrefix, r.controllerInfo.ControllerID.String()) + return fmt.Sprintf("%s=%s", controllerLabelPrefix, r.controllerInfo.ControllerID.String()) } func (r *basePoolManager) updateArgsFromProviderInstance(providerInstance commonParams.ProviderInstance) params.UpdateInstanceParams { @@ -1366,6 +1368,19 @@ func (r *basePoolManager) deleteInstanceFromProvider(ctx context.Context, instan return nil } +func (r *basePoolManager) sleepWithCancel(sleepTime time.Duration) (canceled bool) { + ticker := time.NewTicker(sleepTime) + defer ticker.Stop() + + select { + case <-ticker.C: + return false + case <-r.quit: + case <-r.ctx.Done(): + } + return true +} + func (r *basePoolManager) deletePendingInstances() error { instances, err := r.store.ListEntityInstances(r.ctx, r.entity) if err != nil { @@ -1414,7 +1429,9 @@ func (r *basePoolManager) deletePendingInstances() error { return fmt.Errorf("failed to generate random number: %w", err) } jitter := time.Duration(num.Int64()) * time.Millisecond - time.Sleep(jitter) + if canceled := r.sleepWithCancel(jitter); canceled { + return nil + } currentStatus := instance.Status deleteMux := false @@ -1598,6 +1615,16 @@ func (r *basePoolManager) Start() error { initialToolUpdate := make(chan struct{}, 1) go func() { slog.Info("running initial tool update") + for { + slog.DebugContext(r.ctx, "waiting for tools to be available") + hasTools, stopped := r.waitForToolsOrCancel() + if stopped { + return + } + if hasTools { + break + } + } if err := r.updateTools(); err != nil { slog.With(slog.Any("error", err)).Error("failed to update tools") } @@ -1789,7 +1816,7 @@ func (r *basePoolManager) consumeQueuedJobs() error { } jobLabels := []string{ - fmt.Sprintf("%s%d", jobLabelPrefix, job.ID), + fmt.Sprintf("%s=%d", jobLabelPrefix, job.ID), } for i := 0; i < poolRR.Len(); i++ { pool, err := poolRR.Next() @@ -1931,15 +1958,15 @@ func (r *basePoolManager) InstallWebhook(ctx context.Context, param params.Insta func (r *basePoolManager) ValidateOwner(job params.WorkflowJob) error { switch r.entity.EntityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: if !strings.EqualFold(job.Repository.Name, r.entity.Name) || !strings.EqualFold(job.Repository.Owner.Login, r.entity.Owner) { return runnerErrors.NewBadRequestError("job not meant for this pool manager") } - case params.GithubEntityTypeOrganization: - if !strings.EqualFold(job.Organization.Login, r.entity.Owner) { + case params.ForgeEntityTypeOrganization: + if !strings.EqualFold(job.GetOrgName(r.entity.Credentials.ForgeType), r.entity.Owner) { return runnerErrors.NewBadRequestError("job not meant for this pool manager") } - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: if !strings.EqualFold(job.Enterprise.Slug, r.entity.Owner) { return runnerErrors.NewBadRequestError("job not meant for this pool manager") } diff --git a/runner/pool/stub_client.go b/runner/pool/stub_client.go index 2518ce9cd..6fa44e74f 100644 --- a/runner/pool/stub_client.go +++ b/runner/pool/stub_client.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package pool import ( @@ -57,8 +71,8 @@ func (s *stubGithubClient) GetWorkflowJobByID(_ context.Context, _, _ string, _ return nil, nil, s.err } -func (s *stubGithubClient) GetEntity() params.GithubEntity { - return params.GithubEntity{} +func (s *stubGithubClient) GetEntity() params.ForgeEntity { + return params.ForgeEntity{} } func (s *stubGithubClient) GithubBaseURL() *url.URL { diff --git a/runner/pool/util.go b/runner/pool/util.go index 9b7b7f148..dd55e1dbd 100644 --- a/runner/pool/util.go +++ b/runner/pool/util.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package pool import ( @@ -5,11 +19,13 @@ import ( "strings" "sync" "sync/atomic" + "time" "github.com/google/go-github/v71/github" runnerErrors "github.com/cloudbase/garm-provider-common/errors" commonParams "github.com/cloudbase/garm-provider-common/params" + "github.com/cloudbase/garm/cache" dbCommon "github.com/cloudbase/garm/database/common" "github.com/cloudbase/garm/database/watcher" "github.com/cloudbase/garm/params" @@ -91,7 +107,8 @@ func instanceInList(instanceName string, instances []commonParams.ProviderInstan func controllerIDFromLabels(labels []string) string { for _, lbl := range labels { if strings.HasPrefix(lbl, controllerLabelPrefix) { - return lbl[len(controllerLabelPrefix):] + trimLength := min(len(controllerLabelPrefix)+1, len(lbl)) + return lbl[trimLength:] } } return "" @@ -119,7 +136,7 @@ func isManagedRunner(labels []string, controllerID string) bool { return runnerControllerID == controllerID } -func composeWatcherFilters(entity params.GithubEntity) dbCommon.PayloadFilterFunc { +func composeWatcherFilters(entity params.ForgeEntity) dbCommon.PayloadFilterFunc { // We want to watch for changes in either the controller or the // entity itself. return watcher.WithAny( @@ -131,6 +148,22 @@ func composeWatcherFilters(entity params.GithubEntity) dbCommon.PayloadFilterFun // Any operation on the entity we're managing the pool for. watcher.WithEntityFilter(entity), // Watch for changes to the github credentials - watcher.WithGithubCredentialsFilter(entity.Credentials), + watcher.WithForgeCredentialsFilter(entity.Credentials), ) } + +func (r *basePoolManager) waitForToolsOrCancel() (hasTools, stopped bool) { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + select { + case <-ticker.C: + if _, err := cache.GetGithubToolsCache(r.entity.ID); err != nil { + return false, false + } + return true, false + case <-r.quit: + return false, true + case <-r.ctx.Done(): + return false, true + } +} diff --git a/runner/pool/util_test.go b/runner/pool/util_test.go index bcfea8791..67d31f764 100644 --- a/runner/pool/util_test.go +++ b/runner/pool/util_test.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package pool import ( diff --git a/runner/pool/watcher.go b/runner/pool/watcher.go index 7f05d93bd..324643ce7 100644 --- a/runner/pool/watcher.go +++ b/runner/pool/watcher.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package pool import ( @@ -14,7 +28,7 @@ import ( // entityGetter is implemented by all github entities (repositories, organizations and enterprises) type entityGetter interface { - GetEntity() (params.GithubEntity, error) + GetEntity() (params.ForgeEntity, error) } func (r *basePoolManager) handleControllerUpdateEvent(controllerInfo params.ControllerInfo) { @@ -38,7 +52,7 @@ func (r *basePoolManager) getClientOrStub() runnerCommon.GithubClient { return ghc } -func (r *basePoolManager) handleEntityUpdate(entity params.GithubEntity, operation common.OperationType) { +func (r *basePoolManager) handleEntityUpdate(entity params.ForgeEntity, operation common.OperationType) { slog.DebugContext(r.ctx, "received entity operation", "entity", entity.ID, "operation", operation) if r.entity.ID != entity.ID { slog.WarnContext(r.ctx, "entity ID mismatch; stale event? refusing to update", "entity", entity.ID) @@ -56,7 +70,7 @@ func (r *basePoolManager) handleEntityUpdate(entity params.GithubEntity, operati return } - credentialsUpdate := r.entity.Credentials.ID != entity.Credentials.ID + credentialsUpdate := r.entity.Credentials.GetID() != entity.Credentials.GetID() defer func() { slog.DebugContext(r.ctx, "deferred tools update", "credentials_update", credentialsUpdate) if !credentialsUpdate { @@ -85,7 +99,7 @@ func (r *basePoolManager) handleEntityUpdate(entity params.GithubEntity, operati slog.DebugContext(r.ctx, "lock released", "entity", entity.ID) } -func (r *basePoolManager) handleCredentialsUpdate(credentials params.GithubCredentials) { +func (r *basePoolManager) handleCredentialsUpdate(credentials params.ForgeCredentials) { // when we switch credentials on an entity (like from one app to another or from an app // to a PAT), we may still get events for the previous credentials as the channel is buffered. // The watcher will watch for changes to the entity itself, which includes events that @@ -97,12 +111,12 @@ func (r *basePoolManager) handleCredentialsUpdate(credentials params.GithubCrede // test-repo. This function would handle situations where "org_pat" is updated. // If "test-repo" is updated with new credentials, that event is handled above in // handleEntityUpdate. - shouldUpdateTools := r.entity.Credentials.ID == credentials.ID + shouldUpdateTools := r.entity.Credentials.GetID() == credentials.GetID() defer func() { if !shouldUpdateTools { return } - slog.DebugContext(r.ctx, "deferred tools update", "credentials_id", credentials.ID) + slog.DebugContext(r.ctx, "deferred tools update", "credentials_id", credentials.GetID()) if err := r.updateTools(); err != nil { slog.ErrorContext(r.ctx, "failed to update tools", "error", err) } @@ -110,12 +124,12 @@ func (r *basePoolManager) handleCredentialsUpdate(credentials params.GithubCrede r.mux.Lock() if !shouldUpdateTools { - slog.InfoContext(r.ctx, "credential ID mismatch; stale event?", "credentials_id", credentials.ID) + slog.InfoContext(r.ctx, "credential ID mismatch; stale event?", "credentials_id", credentials.GetID()) r.mux.Unlock() return } - slog.DebugContext(r.ctx, "updating credentials", "credentials_id", credentials.ID) + slog.DebugContext(r.ctx, "updating credentials", "credentials_id", credentials.GetID()) r.entity.Credentials = credentials r.ghcli = r.getClientOrStub() r.mux.Unlock() @@ -124,8 +138,8 @@ func (r *basePoolManager) handleCredentialsUpdate(credentials params.GithubCrede func (r *basePoolManager) handleWatcherEvent(event common.ChangePayload) { dbEntityType := common.DatabaseEntityType(r.entity.EntityType) switch event.EntityType { - case common.GithubCredentialsEntityType: - credentials, ok := event.Payload.(params.GithubCredentials) + case common.GithubCredentialsEntityType, common.GiteaCredentialsEntityType: + credentials, ok := event.Payload.(params.ForgeCredentials) if !ok { slog.ErrorContext(r.ctx, "failed to cast payload to github credentials") return diff --git a/runner/pools_test.go b/runner/pools_test.go index 918598d19..3bc5d4b3e 100644 --- a/runner/pools_test.go +++ b/runner/pools_test.go @@ -47,9 +47,9 @@ type PoolTestSuite struct { Runner *Runner adminCtx context.Context - testCreds params.GithubCredentials - secondaryTestCreds params.GithubCredentials - githubEndpoint params.GithubEndpoint + testCreds params.ForgeCredentials + secondaryTestCreds params.ForgeCredentials + githubEndpoint params.ForgeEndpoint } func (s *PoolTestSuite) SetupTest() { @@ -69,15 +69,15 @@ func (s *PoolTestSuite) SetupTest() { s.secondaryTestCreds = garmTesting.CreateTestGithubCredentials(s.adminCtx, "secondary-creds", db, s.T(), s.githubEndpoint) // create an organization for testing purposes - org, err := db.CreateOrganization(s.adminCtx, "test-org", s.testCreds.Name, "test-webhookSecret", params.PoolBalancerTypeRoundRobin) + org, err := db.CreateOrganization(s.adminCtx, "test-org", s.testCreds, "test-webhookSecret", params.PoolBalancerTypeRoundRobin) if err != nil { s.FailNow(fmt.Sprintf("failed to create org: %s", err)) } // create some pool objects in the database, for testing purposes - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: org.ID, - EntityType: params.GithubEntityTypeOrganization, + EntityType: params.ForgeEntityTypeOrganization, } orgPools := []params.Pool{} for i := 1; i <= 3; i++ { diff --git a/runner/providers/common/common.go b/runner/providers/common/common.go index 4e49e080d..f1a5a66d9 100644 --- a/runner/providers/common/common.go +++ b/runner/providers/common/common.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package common import ( diff --git a/runner/providers/external/external.go b/runner/providers/external/external.go index 23b9b8944..46e3dd475 100644 --- a/runner/providers/external/external.go +++ b/runner/providers/external/external.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package external import ( diff --git a/runner/providers/util/util.go b/runner/providers/util/util.go index 2948730b6..fb3c12bd0 100644 --- a/runner/providers/util/util.go +++ b/runner/providers/util/util.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package util import ( diff --git a/runner/providers/v0.1.0/external.go b/runner/providers/v0.1.0/external.go index 6dd0ef46f..60c5ca1b9 100644 --- a/runner/providers/v0.1.0/external.go +++ b/runner/providers/v0.1.0/external.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package v010 import ( diff --git a/runner/providers/v0.1.1/external.go b/runner/providers/v0.1.1/external.go index 530a26455..192f735de 100644 --- a/runner/providers/v0.1.1/external.go +++ b/runner/providers/v0.1.1/external.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package v011 import ( diff --git a/runner/repositories.go b/runner/repositories.go index ab4f8e908..058e1a026 100644 --- a/runner/repositories.go +++ b/runner/repositories.go @@ -38,7 +38,16 @@ func (r *Runner) CreateRepository(ctx context.Context, param params.CreateRepoPa return params.Repository{}, errors.Wrap(err, "validating params") } - creds, err := r.store.GetGithubCredentialsByName(ctx, param.CredentialsName, true) + var creds params.ForgeCredentials + switch param.GetForgeType() { + case params.GithubEndpointType: + creds, err = r.store.GetGithubCredentialsByName(ctx, param.CredentialsName, true) + case params.GiteaEndpointType: + creds, err = r.store.GetGiteaCredentialsByName(ctx, param.CredentialsName, true) + default: + return params.Repository{}, runnerErrors.NewBadRequestError("invalid forge type: %s", param.GetForgeType()) + } + if err != nil { return params.Repository{}, runnerErrors.NewBadRequestError("credentials %s not defined", param.CredentialsName) } @@ -52,7 +61,7 @@ func (r *Runner) CreateRepository(ctx context.Context, param params.CreateRepoPa return params.Repository{}, runnerErrors.NewConflictError("repository %s/%s already exists", param.Owner, param.Name) } - repo, err = r.store.CreateRepository(ctx, param.Owner, param.Name, creds.Name, param.WebhookSecret, param.PoolBalancerType) + repo, err = r.store.CreateRepository(ctx, param.Owner, param.Name, creds, param.WebhookSecret, param.PoolBalancerType) if err != nil { return params.Repository{}, errors.Wrap(err, "creating repository") } @@ -235,9 +244,9 @@ func (r *Runner) CreateRepoPool(ctx context.Context, repoID string, param params createPoolParams.RunnerBootstrapTimeout = appdefaults.DefaultRunnerBootstrapTimeout } - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: repoID, - EntityType: params.GithubEntityTypeRepository, + EntityType: params.ForgeEntityTypeRepository, } pool, err := r.store.CreateEntityPool(ctx, entity, createPoolParams) @@ -253,9 +262,9 @@ func (r *Runner) GetRepoPoolByID(ctx context.Context, repoID, poolID string) (pa return params.Pool{}, runnerErrors.ErrUnauthorized } - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: repoID, - EntityType: params.GithubEntityTypeRepository, + EntityType: params.ForgeEntityTypeRepository, } pool, err := r.store.GetEntityPool(ctx, entity, poolID) @@ -271,9 +280,9 @@ func (r *Runner) DeleteRepoPool(ctx context.Context, repoID, poolID string) erro return runnerErrors.ErrUnauthorized } - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: repoID, - EntityType: params.GithubEntityTypeRepository, + EntityType: params.ForgeEntityTypeRepository, } pool, err := r.store.GetEntityPool(ctx, entity, poolID) if err != nil { @@ -300,9 +309,9 @@ func (r *Runner) ListRepoPools(ctx context.Context, repoID string) ([]params.Poo if !auth.IsAdmin(ctx) { return []params.Pool{}, runnerErrors.ErrUnauthorized } - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: repoID, - EntityType: params.GithubEntityTypeRepository, + EntityType: params.ForgeEntityTypeRepository, } pools, err := r.store.ListEntityPools(ctx, entity) if err != nil { @@ -328,9 +337,9 @@ func (r *Runner) UpdateRepoPool(ctx context.Context, repoID, poolID string, para return params.Pool{}, runnerErrors.ErrUnauthorized } - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: repoID, - EntityType: params.GithubEntityTypeRepository, + EntityType: params.ForgeEntityTypeRepository, } pool, err := r.store.GetEntityPool(ctx, entity, poolID) if err != nil { @@ -362,9 +371,9 @@ func (r *Runner) ListRepoInstances(ctx context.Context, repoID string) ([]params if !auth.IsAdmin(ctx) { return nil, runnerErrors.ErrUnauthorized } - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: repoID, - EntityType: params.GithubEntityTypeRepository, + EntityType: params.ForgeEntityTypeRepository, } instances, err := r.store.ListEntityInstances(ctx, entity) if err != nil { diff --git a/runner/repositories_test.go b/runner/repositories_test.go index c1aa04b44..0adf40d7f 100644 --- a/runner/repositories_test.go +++ b/runner/repositories_test.go @@ -39,7 +39,7 @@ type RepoTestFixtures struct { Store dbCommon.Store StoreRepos map[string]params.Repository Providers map[string]common.Provider - Credentials map[string]params.GithubCredentials + Credentials map[string]params.ForgeCredentials CreateRepoParams params.CreateRepoParams CreatePoolParams params.CreatePoolParams CreateInstanceParams params.CreateInstanceParams @@ -60,9 +60,9 @@ type RepoTestSuite struct { Fixtures *RepoTestFixtures Runner *Runner - testCreds params.GithubCredentials - secondaryTestCreds params.GithubCredentials - githubEndpoint params.GithubEndpoint + testCreds params.ForgeCredentials + secondaryTestCreds params.ForgeCredentials + githubEndpoint params.ForgeEndpoint } func (s *RepoTestSuite) SetupTest() { @@ -86,7 +86,7 @@ func (s *RepoTestSuite) SetupTest() { adminCtx, fmt.Sprintf("test-owner-%v", i), name, - s.testCreds.Name, + s.testCreds, fmt.Sprintf("test-webhook-secret-%v", i), params.PoolBalancerTypeRoundRobin, ) @@ -107,7 +107,7 @@ func (s *RepoTestSuite) SetupTest() { Providers: map[string]common.Provider{ "test-provider": providerMock, }, - Credentials: map[string]params.GithubCredentials{ + Credentials: map[string]params.ForgeCredentials{ s.testCreds.Name: s.testCreds, s.secondaryTestCreds.Name: s.secondaryTestCreds, }, @@ -116,6 +116,7 @@ func (s *RepoTestSuite) SetupTest() { Name: "test-repo-create", CredentialsName: s.testCreds.Name, WebhookSecret: "test-create-repo-webhook-secret", + ForgeType: params.GithubEndpointType, }, CreatePoolParams: params.CreatePoolParams{ ProviderName: "test-provider", @@ -299,9 +300,9 @@ func (s *RepoTestSuite) TestDeleteRepositoryErrUnauthorized() { } func (s *RepoTestSuite) TestDeleteRepositoryPoolDefinedFailed() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreRepos["test-repo-1"].ID, - EntityType: params.GithubEntityTypeRepository, + EntityType: params.ForgeEntityTypeRepository, } pool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { @@ -419,9 +420,9 @@ func (s *RepoTestSuite) TestCreateRepoPoolFetchPoolParamsFailed() { } func (s *RepoTestSuite) TestGetRepoPoolByID() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreRepos["test-repo-1"].ID, - EntityType: params.GithubEntityTypeRepository, + EntityType: params.ForgeEntityTypeRepository, } repoPool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { @@ -441,9 +442,9 @@ func (s *RepoTestSuite) TestGetRepoPoolByIDErrUnauthorized() { } func (s *RepoTestSuite) TestDeleteRepoPool() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreRepos["test-repo-1"].ID, - EntityType: params.GithubEntityTypeRepository, + EntityType: params.ForgeEntityTypeRepository, } pool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { @@ -465,9 +466,9 @@ func (s *RepoTestSuite) TestDeleteRepoPoolErrUnauthorized() { } func (s *RepoTestSuite) TestDeleteRepoPoolRunnersFailed() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreRepos["test-repo-1"].ID, - EntityType: params.GithubEntityTypeRepository, + EntityType: params.ForgeEntityTypeRepository, } pool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { @@ -484,9 +485,9 @@ func (s *RepoTestSuite) TestDeleteRepoPoolRunnersFailed() { } func (s *RepoTestSuite) TestListRepoPools() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreRepos["test-repo-1"].ID, - EntityType: params.GithubEntityTypeRepository, + EntityType: params.ForgeEntityTypeRepository, } repoPools := []params.Pool{} for i := 1; i <= 2; i++ { @@ -511,9 +512,9 @@ func (s *RepoTestSuite) TestListRepoPoolsErrUnauthorized() { } func (s *RepoTestSuite) TestListPoolInstances() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreRepos["test-repo-1"].ID, - EntityType: params.GithubEntityTypeRepository, + EntityType: params.ForgeEntityTypeRepository, } pool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { @@ -542,9 +543,9 @@ func (s *RepoTestSuite) TestListPoolInstancesErrUnauthorized() { } func (s *RepoTestSuite) TestUpdateRepoPool() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreRepos["test-repo-1"].ID, - EntityType: params.GithubEntityTypeRepository, + EntityType: params.ForgeEntityTypeRepository, } repoPool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { @@ -565,9 +566,9 @@ func (s *RepoTestSuite) TestUpdateRepoPoolErrUnauthorized() { } func (s *RepoTestSuite) TestUpdateRepoPoolMinIdleGreaterThanMax() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreRepos["test-repo-1"].ID, - EntityType: params.GithubEntityTypeRepository, + EntityType: params.ForgeEntityTypeRepository, } pool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { @@ -584,9 +585,9 @@ func (s *RepoTestSuite) TestUpdateRepoPoolMinIdleGreaterThanMax() { } func (s *RepoTestSuite) TestListRepoInstances() { - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: s.Fixtures.StoreRepos["test-repo-1"].ID, - EntityType: params.GithubEntityTypeRepository, + EntityType: params.ForgeEntityTypeRepository, } pool, err := s.Fixtures.Store.CreateEntityPool(s.Fixtures.AdminContext, entity, s.Fixtures.CreatePoolParams) if err != nil { diff --git a/runner/runner.go b/runner/runner.go index 42a955fc2..aa55ee4f9 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -602,10 +602,10 @@ func (r *Runner) validateHookBody(signature, secret string, body []byte) error { return nil } -func (r *Runner) findEndpointForJob(job params.WorkflowJob) (params.GithubEndpoint, error) { +func (r *Runner) findEndpointForJob(job params.WorkflowJob, forgeType params.EndpointType) (params.ForgeEndpoint, error) { uri, err := url.ParseRequestURI(job.WorkflowJob.HTMLURL) if err != nil { - return params.GithubEndpoint{}, errors.Wrap(err, "parsing job URL") + return params.ForgeEndpoint{}, errors.Wrap(err, "parsing job URL") } baseURI := fmt.Sprintf("%s://%s", uri.Scheme, uri.Host) @@ -614,31 +614,45 @@ func (r *Runner) findEndpointForJob(job params.WorkflowJob) (params.GithubEndpoi // a GHES involved, those users will have just one extra endpoint or 2 (if they also have a // test env). But there should be a relatively small number, regardless. So we don't really care // that much about the performance of this function. - endpoints, err := r.store.ListGithubEndpoints(r.ctx) + var endpoints []params.ForgeEndpoint + switch forgeType { + case params.GithubEndpointType: + endpoints, err = r.store.ListGithubEndpoints(r.ctx) + case params.GiteaEndpointType: + endpoints, err = r.store.ListGiteaEndpoints(r.ctx) + default: + return params.ForgeEndpoint{}, runnerErrors.NewBadRequestError("unknown forge type %s", forgeType) + } + if err != nil { - return params.GithubEndpoint{}, errors.Wrap(err, "fetching github endpoints") + return params.ForgeEndpoint{}, errors.Wrap(err, "fetching github endpoints") } for _, ep := range endpoints { - if ep.BaseURL == baseURI { + slog.DebugContext(r.ctx, "checking endpoint", "base_uri", baseURI, "endpoint", ep.BaseURL) + epBaseURI := strings.TrimSuffix(ep.BaseURL, "/") + if epBaseURI == baseURI { return ep, nil } } - return params.GithubEndpoint{}, runnerErrors.NewNotFoundError("no endpoint found for job") + return params.ForgeEndpoint{}, runnerErrors.NewNotFoundError("no endpoint found for job") } -func (r *Runner) DispatchWorkflowJob(hookTargetType, signature string, jobData []byte) error { +func (r *Runner) DispatchWorkflowJob(hookTargetType, signature string, forgeType params.EndpointType, jobData []byte) error { if len(jobData) == 0 { + slog.ErrorContext(r.ctx, "missing job data") return runnerErrors.NewBadRequestError("missing job data") } var job params.WorkflowJob if err := json.Unmarshal(jobData, &job); err != nil { + slog.ErrorContext(r.ctx, "failed to unmarshal job data", "error", err) return errors.Wrapf(runnerErrors.ErrBadRequest, "invalid job data: %s", err) } - endpoint, err := r.findEndpointForJob(job) + endpoint, err := r.findEndpointForJob(job, forgeType) if err != nil { + slog.ErrorContext(r.ctx, "failed to find endpoint for job", "error", err) return errors.Wrap(err, "finding endpoint for job") } @@ -649,23 +663,28 @@ func (r *Runner) DispatchWorkflowJob(hookTargetType, signature string, jobData [ slog.DebugContext( r.ctx, "got hook for repo", "repo_owner", util.SanitizeLogEntry(job.Repository.Owner.Login), - "repo_name", util.SanitizeLogEntry(job.Repository.Name)) + "repo_name", util.SanitizeLogEntry(job.Repository.Name), + "endpoint", endpoint.Name) poolManager, err = r.findRepoPoolManager(job.Repository.Owner.Login, job.Repository.Name, endpoint.Name) case OrganizationHook: slog.DebugContext( r.ctx, "got hook for organization", - "organization", util.SanitizeLogEntry(job.Organization.Login)) - poolManager, err = r.findOrgPoolManager(job.Organization.Login, endpoint.Name) + "organization", util.SanitizeLogEntry(job.GetOrgName(forgeType)), + "endpoint", endpoint.Name) + poolManager, err = r.findOrgPoolManager(job.GetOrgName(forgeType), endpoint.Name) case EnterpriseHook: slog.DebugContext( r.ctx, "got hook for enterprise", - "enterprise", util.SanitizeLogEntry(job.Enterprise.Slug)) + "enterprise", util.SanitizeLogEntry(job.Enterprise.Slug), + "endpoint", endpoint.Name) poolManager, err = r.findEnterprisePoolManager(job.Enterprise.Slug, endpoint.Name) default: return runnerErrors.NewBadRequestError("cannot handle hook target type %s", hookTargetType) } + slog.DebugContext(r.ctx, "found pool manager", "pool_manager", poolManager.ID()) if err != nil { + slog.ErrorContext(r.ctx, "failed to find pool manager", "error", err, "hook_target_type", hookTargetType) // We don't have a repository or organization configured that // can handle this workflow job. return errors.Wrap(err, "fetching poolManager") @@ -675,10 +694,12 @@ func (r *Runner) DispatchWorkflowJob(hookTargetType, signature string, jobData [ // we make sure that the source of this workflow job is valid. secret := poolManager.WebhookSecret() if err := r.validateHookBody(signature, secret, jobData); err != nil { + slog.ErrorContext(r.ctx, "failed to validate webhook data", "error", err) return errors.Wrap(err, "validating webhook data") } if err := poolManager.HandleWorkflowJob(job); err != nil { + slog.ErrorContext(r.ctx, "failed to handle workflow job", "error", err) return errors.Wrap(err, "handling workflow job") } @@ -867,15 +888,17 @@ func (r *Runner) DeleteRunner(ctx context.Context, instanceName string, forceDel } if err != nil { - if errors.Is(err, runnerErrors.ErrUnauthorized) && instance.PoolID != "" { - poolMgr, err := r.getPoolManagerFromInstance(ctx, instance) - if err != nil { - return errors.Wrap(err, "fetching pool manager for instance") + if !errors.Is(err, runnerErrors.ErrNotFound) { + if errors.Is(err, runnerErrors.ErrUnauthorized) && instance.PoolID != "" { + poolMgr, err := r.getPoolManagerFromInstance(ctx, instance) + if err != nil { + return errors.Wrap(err, "fetching pool manager for instance") + } + poolMgr.SetPoolRunningState(false, fmt.Sprintf("failed to remove runner: %q", err)) + } + if !bypassGithubUnauthorized { + return errors.Wrap(err, "removing runner from github") } - poolMgr.SetPoolRunningState(false, fmt.Sprintf("failed to remove runner: %q", err)) - } - if !bypassGithubUnauthorized { - return errors.Wrap(err, "removing runner from github") } } } @@ -928,7 +951,7 @@ func (r *Runner) getGHCliFromInstance(ctx context.Context, instance params.Insta } // Fetching the entity from the database will populate all fields, including credentials. - entity, err = r.store.GetGithubEntity(ctx, entity.EntityType, entity.ID) + entity, err = r.store.GetForgeEntity(ctx, entity.EntityType, entity.ID) if err != nil { return nil, nil, errors.Wrap(err, "fetching entity") } diff --git a/runner/scalesets.go b/runner/scalesets.go index 83432e638..e7af9c220 100644 --- a/runner/scalesets.go +++ b/runner/scalesets.go @@ -16,7 +16,6 @@ package runner import ( "context" - "encoding/json" "fmt" "log/slog" @@ -80,7 +79,7 @@ func (r *Runner) DeleteScaleSetByID(ctx context.Context, scaleSetID uint) error return errors.Wrap(err, "getting entity") } - entity, err := r.store.GetGithubEntity(ctx, paramEntity.EntityType, paramEntity.ID) + entity, err := r.store.GetForgeEntity(ctx, paramEntity.EntityType, paramEntity.ID) if err != nil { return errors.Wrap(err, "getting entity") } @@ -143,7 +142,7 @@ func (r *Runner) UpdateScaleSetByID(ctx context.Context, scaleSetID uint, param return params.ScaleSet{}, errors.Wrap(err, "getting entity") } - entity, err := r.store.GetGithubEntity(ctx, paramEntity.EntityType, paramEntity.ID) + entity, err := r.store.GetForgeEntity(ctx, paramEntity.EntityType, paramEntity.ID) if err != nil { return params.ScaleSet{}, errors.Wrap(err, "getting entity") } @@ -181,12 +180,10 @@ func (r *Runner) UpdateScaleSetByID(ctx context.Context, scaleSetID uint, param } if hasUpdates { - result, err := scalesetCli.UpdateRunnerScaleSet(ctx, newSet.ScaleSetID, updateParams) + _, err := scalesetCli.UpdateRunnerScaleSet(ctx, newSet.ScaleSetID, updateParams) if err != nil { return fmt.Errorf("failed to update scaleset in github: %w", err) } - asJs, _ := json.MarshalIndent(result, "", " ") - slog.Info("update result", "data", string(asJs)) } return nil } @@ -198,7 +195,7 @@ func (r *Runner) UpdateScaleSetByID(ctx context.Context, scaleSetID uint, param return newScaleSet, nil } -func (r *Runner) CreateEntityScaleSet(ctx context.Context, entityType params.GithubEntityType, entityID string, param params.CreateScaleSetParams) (scaleSetRet params.ScaleSet, err error) { +func (r *Runner) CreateEntityScaleSet(ctx context.Context, entityType params.ForgeEntityType, entityID string, param params.CreateScaleSetParams) (scaleSetRet params.ScaleSet, err error) { if !auth.IsAdmin(ctx) { return params.ScaleSet{}, runnerErrors.ErrUnauthorized } @@ -211,11 +208,15 @@ func (r *Runner) CreateEntityScaleSet(ctx context.Context, entityType params.Git param.GitHubRunnerGroup = "Default" } - entity, err := r.store.GetGithubEntity(ctx, entityType, entityID) + entity, err := r.store.GetForgeEntity(ctx, entityType, entityID) if err != nil { return params.ScaleSet{}, errors.Wrap(err, "getting entity") } + if entity.Credentials.ForgeType != params.GithubEndpointType { + return params.ScaleSet{}, runnerErrors.NewBadRequestError("scale sets are only supported for github entities") + } + ghCli, err := github.Client(ctx, entity) if err != nil { return params.ScaleSet{}, errors.Wrap(err, "creating github client") @@ -255,9 +256,6 @@ func (r *Runner) CreateEntityScaleSet(ctx context.Context, entityType params.Git return params.ScaleSet{}, errors.Wrap(err, "creating runner scale set") } - asJs, _ := json.MarshalIndent(runnerScaleSet, "", " ") - slog.InfoContext(ctx, "scale set", "data", string(asJs)) - defer func() { if err != nil { if innerErr := scalesetCli.DeleteRunnerScaleSet(ctx, runnerScaleSet.ID); innerErr != nil { @@ -287,11 +285,11 @@ func (r *Runner) ListScaleSetInstances(ctx context.Context, scalesetID uint) ([] return instances, nil } -func (r *Runner) ListEntityScaleSets(ctx context.Context, entityType params.GithubEntityType, entityID string) ([]params.ScaleSet, error) { +func (r *Runner) ListEntityScaleSets(ctx context.Context, entityType params.ForgeEntityType, entityID string) ([]params.ScaleSet, error) { if !auth.IsAdmin(ctx) { return []params.ScaleSet{}, runnerErrors.ErrUnauthorized } - entity := params.GithubEntity{ + entity := params.ForgeEntity{ ID: entityID, EntityType: entityType, } diff --git a/test/integration/client_utils.go b/test/integration/client_utils.go index a0f17893c..e423c107b 100644 --- a/test/integration/client_utils.go +++ b/test/integration/client_utils.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package integration import ( @@ -51,7 +65,7 @@ func listCredentials(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfo return listCredentialsResponse.Payload, nil } -func createGithubCredentials(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, credentialsParams params.CreateGithubCredentialsParams) (*params.GithubCredentials, error) { +func createGithubCredentials(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, credentialsParams params.CreateGithubCredentialsParams) (*params.ForgeCredentials, error) { createCredentialsResponse, err := apiCli.Credentials.CreateCredentials( clientCredentials.NewCreateCredentialsParams().WithBody(credentialsParams), apiAuthToken) @@ -67,7 +81,7 @@ func deleteGithubCredentials(apiCli *client.GarmAPI, apiAuthToken runtime.Client apiAuthToken) } -func updateGithubCredentials(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, credentialsID int64, credentialsParams params.UpdateGithubCredentialsParams) (*params.GithubCredentials, error) { +func updateGithubCredentials(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, credentialsID int64, credentialsParams params.UpdateGithubCredentialsParams) (*params.ForgeCredentials, error) { updateCredentialsResponse, err := apiCli.Credentials.UpdateCredentials( clientCredentials.NewUpdateCredentialsParams().WithID(credentialsID).WithBody(credentialsParams), apiAuthToken) @@ -77,7 +91,7 @@ func updateGithubCredentials(apiCli *client.GarmAPI, apiAuthToken runtime.Client return &updateCredentialsResponse.Payload, nil } -func createGithubEndpoint(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, endpointParams params.CreateGithubEndpointParams) (*params.GithubEndpoint, error) { +func createGithubEndpoint(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, endpointParams params.CreateGithubEndpointParams) (*params.ForgeEndpoint, error) { createEndpointResponse, err := apiCli.Endpoints.CreateGithubEndpoint( clientEndpoints.NewCreateGithubEndpointParams().WithBody(endpointParams), apiAuthToken) @@ -87,7 +101,7 @@ func createGithubEndpoint(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAut return &createEndpointResponse.Payload, nil } -func listGithubEndpoints(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter) (params.GithubEndpoints, error) { +func listGithubEndpoints(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter) (params.ForgeEndpoints, error) { listEndpointsResponse, err := apiCli.Endpoints.ListGithubEndpoints( clientEndpoints.NewListGithubEndpointsParams(), apiAuthToken) @@ -97,7 +111,7 @@ func listGithubEndpoints(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuth return listEndpointsResponse.Payload, nil } -func getGithubEndpoint(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, endpointName string) (*params.GithubEndpoint, error) { +func getGithubEndpoint(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, endpointName string) (*params.ForgeEndpoint, error) { getEndpointResponse, err := apiCli.Endpoints.GetGithubEndpoint( clientEndpoints.NewGetGithubEndpointParams().WithName(endpointName), apiAuthToken) @@ -113,7 +127,7 @@ func deleteGithubEndpoint(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAut apiAuthToken) } -func updateGithubEndpoint(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, endpointName string, endpointParams params.UpdateGithubEndpointParams) (*params.GithubEndpoint, error) { +func updateGithubEndpoint(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, endpointName string, endpointParams params.UpdateGithubEndpointParams) (*params.ForgeEndpoint, error) { updateEndpointResponse, err := apiCli.Endpoints.UpdateGithubEndpoint( clientEndpoints.NewUpdateGithubEndpointParams().WithName(endpointName).WithBody(endpointParams), apiAuthToken) diff --git a/test/integration/credentials_test.go b/test/integration/credentials_test.go index 8d92bf226..9b9387f68 100644 --- a/test/integration/credentials_test.go +++ b/test/integration/credentials_test.go @@ -1,6 +1,19 @@ //go:build integration // +build integration +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package integration import ( @@ -25,7 +38,7 @@ func (suite *GarmSuite) TestGithubCredentialsErrorOnDuplicateCredentialsName() { Name: dummyCredentialsName, Endpoint: defaultEndpointName, Description: "GARM test credentials", - AuthType: params.GithubAuthTypePAT, + AuthType: params.ForgeAuthTypePAT, PAT: params.GithubPAT{ OAuth2Token: "dummy", }, @@ -68,7 +81,7 @@ func (suite *GarmSuite) TestGithubCredentialsFailsOnInvalidAuthType() { Name: dummyCredentialsName, Endpoint: defaultEndpointName, Description: "GARM test credentials", - AuthType: params.GithubAuthType("invalid"), + AuthType: params.ForgeAuthType("invalid"), PAT: params.GithubPAT{ OAuth2Token: "dummy", }, @@ -87,7 +100,7 @@ func (suite *GarmSuite) TestGithubCredentialsFailsWhenAuthTypeParamsAreIncorrect Name: dummyCredentialsName, Endpoint: defaultEndpointName, Description: "GARM test credentials", - AuthType: params.GithubAuthTypePAT, + AuthType: params.ForgeAuthTypePAT, App: params.GithubApp{ AppID: 123, InstallationID: 456, @@ -107,7 +120,7 @@ func (suite *GarmSuite) TestGithubCredentialsFailsWhenAuthTypeParamsAreMissing() Name: dummyCredentialsName, Endpoint: defaultEndpointName, Description: "GARM test credentials", - AuthType: params.GithubAuthTypeApp, + AuthType: params.ForgeAuthTypeApp, } _, err := createGithubCredentials(suite.cli, suite.authToken, createCredsParams) suite.Error(err, "expected error when creating credentials with missing auth type params") @@ -147,7 +160,7 @@ func (suite *GarmSuite) TestGithubCredentialsFailWhenAppKeyIsInvalid() { Name: dummyCredentialsName, Endpoint: defaultEndpointName, Description: "GARM test credentials", - AuthType: params.GithubAuthTypeApp, + AuthType: params.ForgeAuthTypeApp, App: params.GithubApp{ AppID: 123, InstallationID: 456, @@ -166,7 +179,7 @@ func (suite *GarmSuite) TestGithubCredentialsFailWhenEndpointDoesntExist() { Name: dummyCredentialsName, Endpoint: "iDontExist.example.com", Description: "GARM test credentials", - AuthType: params.GithubAuthTypePAT, + AuthType: params.ForgeAuthTypePAT, PAT: params.GithubPAT{ OAuth2Token: "dummy", }, @@ -189,7 +202,7 @@ func (suite *GarmSuite) TestGithubCredentialsFailsOnDuplicateName() { Name: dummyCredentialsName, Endpoint: defaultEndpointName, Description: "GARM test credentials", - AuthType: params.GithubAuthTypePAT, + AuthType: params.ForgeAuthTypePAT, PAT: params.GithubPAT{ OAuth2Token: "dummy", }, @@ -199,12 +212,12 @@ func (suite *GarmSuite) TestGithubCredentialsFailsOnDuplicateName() { expectAPIStatusCode(err, 409) } -func (suite *GarmSuite) createDummyCredentials(name, endpointName string) (*params.GithubCredentials, error) { +func (suite *GarmSuite) createDummyCredentials(name, endpointName string) (*params.ForgeCredentials, error) { createCredsParams := params.CreateGithubCredentialsParams{ Name: name, Endpoint: endpointName, Description: "GARM test credentials", - AuthType: params.GithubAuthTypePAT, + AuthType: params.ForgeAuthTypePAT, PAT: params.GithubPAT{ OAuth2Token: "dummy", }, @@ -212,7 +225,7 @@ func (suite *GarmSuite) createDummyCredentials(name, endpointName string) (*para return suite.CreateGithubCredentials(createCredsParams) } -func (suite *GarmSuite) CreateGithubCredentials(credentialsParams params.CreateGithubCredentialsParams) (*params.GithubCredentials, error) { +func (suite *GarmSuite) CreateGithubCredentials(credentialsParams params.CreateGithubCredentialsParams) (*params.ForgeCredentials, error) { t := suite.T() t.Log("Create GitHub credentials") credentials, err := createGithubCredentials(suite.cli, suite.authToken, credentialsParams) diff --git a/test/integration/endpoints.go b/test/integration/endpoints.go index 9e47d8547..720f43d2b 100644 --- a/test/integration/endpoints.go +++ b/test/integration/endpoints.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package integration import ( @@ -8,7 +22,7 @@ import ( "github.com/cloudbase/garm/params" ) -func checkEndpointParamsAreEqual(a, b params.GithubEndpoint) error { +func checkEndpointParamsAreEqual(a, b params.ForgeEndpoint) error { if a.Name != b.Name { return fmt.Errorf("endpoint name mismatch") } diff --git a/test/integration/endpoints_test.go b/test/integration/endpoints_test.go index e09916bc0..fe0dd1607 100644 --- a/test/integration/endpoints_test.go +++ b/test/integration/endpoints_test.go @@ -1,6 +1,20 @@ //go:build integration // +build integration +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package integration import ( @@ -163,7 +177,7 @@ func (suite *GarmSuite) MustDefaultGithubEndpoint() { suite.Equal(ep.Name, "github.com", "default GitHub endpoint name mismatch") } -func (suite *GarmSuite) GetGithubEndpoint(name string) *params.GithubEndpoint { +func (suite *GarmSuite) GetGithubEndpoint(name string) *params.ForgeEndpoint { t := suite.T() t.Log("Get GitHub endpoint") endpoint, err := getGithubEndpoint(suite.cli, suite.authToken, name) @@ -172,7 +186,7 @@ func (suite *GarmSuite) GetGithubEndpoint(name string) *params.GithubEndpoint { return endpoint } -func (suite *GarmSuite) CreateGithubEndpoint(params params.CreateGithubEndpointParams) (*params.GithubEndpoint, error) { +func (suite *GarmSuite) CreateGithubEndpoint(params params.CreateGithubEndpointParams) (*params.ForgeEndpoint, error) { t := suite.T() t.Log("Create GitHub endpoint") endpoint, err := createGithubEndpoint(suite.cli, suite.authToken, params) @@ -190,7 +204,7 @@ func (suite *GarmSuite) DeleteGithubEndpoint(name string) error { return nil } -func (suite *GarmSuite) ListGithubEndpoints() params.GithubEndpoints { +func (suite *GarmSuite) ListGithubEndpoints() params.ForgeEndpoints { t := suite.T() t.Log("List GitHub endpoints") endpoints, err := listGithubEndpoints(suite.cli, suite.authToken) @@ -199,7 +213,7 @@ func (suite *GarmSuite) ListGithubEndpoints() params.GithubEndpoints { return endpoints } -func (suite *GarmSuite) createDummyEndpoint(name string) (*params.GithubEndpoint, error) { +func (suite *GarmSuite) createDummyEndpoint(name string) (*params.ForgeEndpoint, error) { endpointParams := params.CreateGithubEndpointParams{ Name: name, Description: "Dummy endpoint", diff --git a/test/integration/external_provider_test.go b/test/integration/external_provider_test.go index ceb5b162e..2c85eb354 100644 --- a/test/integration/external_provider_test.go +++ b/test/integration/external_provider_test.go @@ -1,6 +1,19 @@ //go:build integration // +build integration +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package integration import ( diff --git a/test/integration/gh_cleanup/main.go b/test/integration/gh_cleanup/main.go index 6ec50304f..95f1aa780 100644 --- a/test/integration/gh_cleanup/main.go +++ b/test/integration/gh_cleanup/main.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package main import ( diff --git a/test/integration/jobs_test.go b/test/integration/jobs_test.go index 4d87c0779..8da94414a 100644 --- a/test/integration/jobs_test.go +++ b/test/integration/jobs_test.go @@ -1,6 +1,19 @@ //go:build integration // +build integration +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package integration import ( diff --git a/test/integration/list_info_test.go b/test/integration/list_info_test.go index 1eef816ea..ddb3ff86e 100644 --- a/test/integration/list_info_test.go +++ b/test/integration/list_info_test.go @@ -1,6 +1,19 @@ //go:build integration // +build integration +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package integration import ( diff --git a/test/integration/organizations_test.go b/test/integration/organizations_test.go index a96e625c0..8acfb6d37 100644 --- a/test/integration/organizations_test.go +++ b/test/integration/organizations_test.go @@ -1,6 +1,19 @@ //go:build integration // +build integration +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package integration import ( diff --git a/test/integration/repositories_test.go b/test/integration/repositories_test.go index 7b396ffc5..43a5d8ec6 100644 --- a/test/integration/repositories_test.go +++ b/test/integration/repositories_test.go @@ -1,6 +1,19 @@ //go:build integration // +build integration +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package integration import ( @@ -22,7 +35,7 @@ func (suite *GarmSuite) EnsureTestCredentials(name string, oauthToken string, en Name: name, Endpoint: endpointName, Description: "GARM test credentials", - AuthType: params.GithubAuthTypePAT, + AuthType: params.ForgeAuthTypePAT, PAT: params.GithubPAT{ OAuth2Token: oauthToken, }, diff --git a/test/integration/suite_test.go b/test/integration/suite_test.go index c2f4bd5f3..ca6b30304 100644 --- a/test/integration/suite_test.go +++ b/test/integration/suite_test.go @@ -1,6 +1,19 @@ //go:build integration // +build integration +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package integration import ( diff --git a/test/integration/utils.go b/test/integration/utils.go index 24e97b7ff..1fa35b5ec 100644 --- a/test/integration/utils.go +++ b/test/integration/utils.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package integration import ( diff --git a/util/appdefaults/appdefaults.go b/util/appdefaults/appdefaults.go index 479db08fa..cc53f7947 100644 --- a/util/appdefaults/appdefaults.go +++ b/util/appdefaults/appdefaults.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package appdefaults import "time" diff --git a/util/github/client.go b/util/github/client.go index 77803f4fe..f25329c7b 100644 --- a/util/github/client.go +++ b/util/github/client.go @@ -22,6 +22,7 @@ import ( "log/slog" "net/http" "net/url" + "strings" "github.com/google/go-github/v71/github" "github.com/pkg/errors" @@ -39,7 +40,7 @@ type githubClient struct { enterprise *github.EnterpriseService rateLimit *github.RateLimitService - entity params.GithubEntity + entity params.ForgeEntity cli *github.Client } @@ -57,9 +58,9 @@ func (g *githubClient) ListEntityHooks(ctx context.Context, opts *github.ListOpt } }() switch g.entity.EntityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: ret, response, err = g.repo.ListHooks(ctx, g.entity.Owner, g.entity.Name, opts) - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: ret, response, err = g.org.ListHooks(ctx, g.entity.Owner, opts) default: return nil, nil, fmt.Errorf("invalid entity type: %s", g.entity.EntityType) @@ -81,9 +82,9 @@ func (g *githubClient) GetEntityHook(ctx context.Context, id int64) (ret *github } }() switch g.entity.EntityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: ret, _, err = g.repo.GetHook(ctx, g.entity.Owner, g.entity.Name, id) - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: ret, _, err = g.org.GetHook(ctx, g.entity.Owner, id) default: return nil, errors.New("invalid entity type") @@ -91,7 +92,7 @@ func (g *githubClient) GetEntityHook(ctx context.Context, id int64) (ret *github return ret, err } -func (g *githubClient) CreateEntityHook(ctx context.Context, hook *github.Hook) (ret *github.Hook, err error) { +func (g *githubClient) createGithubEntityHook(ctx context.Context, hook *github.Hook) (ret *github.Hook, err error) { metrics.GithubOperationCount.WithLabelValues( "CreateHook", // label: operation g.entity.LabelScope(), // label: scope @@ -105,9 +106,9 @@ func (g *githubClient) CreateEntityHook(ctx context.Context, hook *github.Hook) } }() switch g.entity.EntityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: ret, _, err = g.repo.CreateHook(ctx, g.entity.Owner, g.entity.Name, hook) - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: ret, _, err = g.org.CreateHook(ctx, g.entity.Owner, hook) default: return nil, errors.New("invalid entity type") @@ -115,6 +116,17 @@ func (g *githubClient) CreateEntityHook(ctx context.Context, hook *github.Hook) return ret, err } +func (g *githubClient) CreateEntityHook(ctx context.Context, hook *github.Hook) (ret *github.Hook, err error) { + switch g.entity.Credentials.ForgeType { + case params.GithubEndpointType: + return g.createGithubEntityHook(ctx, hook) + case params.GiteaEndpointType: + return g.createGiteaEntityHook(ctx, hook) + default: + return nil, errors.New("invalid entity type") + } +} + func (g *githubClient) DeleteEntityHook(ctx context.Context, id int64) (ret *github.Response, err error) { metrics.GithubOperationCount.WithLabelValues( "DeleteHook", // label: operation @@ -129,9 +141,9 @@ func (g *githubClient) DeleteEntityHook(ctx context.Context, id int64) (ret *git } }() switch g.entity.EntityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: ret, err = g.repo.DeleteHook(ctx, g.entity.Owner, g.entity.Name, id) - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: ret, err = g.org.DeleteHook(ctx, g.entity.Owner, id) default: return nil, errors.New("invalid entity type") @@ -153,9 +165,9 @@ func (g *githubClient) PingEntityHook(ctx context.Context, id int64) (ret *githu } }() switch g.entity.EntityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: ret, err = g.repo.PingHook(ctx, g.entity.Owner, g.entity.Name, id) - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: ret, err = g.org.PingHook(ctx, g.entity.Owner, id) default: return nil, errors.New("invalid entity type") @@ -182,11 +194,11 @@ func (g *githubClient) ListEntityRunners(ctx context.Context, opts *github.ListR }() switch g.entity.EntityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: ret, response, err = g.ListRunners(ctx, g.entity.Owner, g.entity.Name, opts) - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: ret, response, err = g.ListOrganizationRunners(ctx, g.entity.Owner, opts) - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: ret, response, err = g.enterprise.ListRunners(ctx, g.entity.Owner, opts) default: return nil, nil, errors.New("invalid entity type") @@ -214,11 +226,11 @@ func (g *githubClient) ListEntityRunnerApplicationDownloads(ctx context.Context) }() switch g.entity.EntityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: ret, response, err = g.ListRunnerApplicationDownloads(ctx, g.entity.Owner, g.entity.Name) - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: ret, response, err = g.ListOrganizationRunnerApplicationDownloads(ctx, g.entity.Owner) - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: ret, response, err = g.enterprise.ListRunnerApplicationDownloads(ctx, g.entity.Owner) default: return nil, nil, errors.New("invalid entity type") @@ -228,6 +240,7 @@ func (g *githubClient) ListEntityRunnerApplicationDownloads(ctx context.Context) } func parseError(response *github.Response, err error) error { + slog.Debug("parsing error", "status_code", response.StatusCode, "response", response, "error", err) switch response.StatusCode { case http.StatusNotFound: return runnerErrors.ErrNotFound @@ -250,6 +263,10 @@ func parseError(response *github.Response, err error) error { case http.StatusUnprocessableEntity: return runnerErrors.ErrBadRequest default: + // ugly hack. Gitea returns 500 if we try to remove a runner that does not exist. + if strings.Contains(err.Error(), "does not exist") { + return runnerErrors.ErrNotFound + } return err } } @@ -277,11 +294,11 @@ func (g *githubClient) RemoveEntityRunner(ctx context.Context, runnerID int64) e }() switch g.entity.EntityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: response, err = g.RemoveRunner(ctx, g.entity.Owner, g.entity.Name, runnerID) - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: response, err = g.RemoveOrganizationRunner(ctx, g.entity.Owner, runnerID) - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: response, err = g.enterprise.RemoveRunner(ctx, g.entity.Owner, runnerID) default: return errors.New("invalid entity type") @@ -313,11 +330,11 @@ func (g *githubClient) CreateEntityRegistrationToken(ctx context.Context) (*gith }() switch g.entity.EntityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: ret, response, err = g.CreateRegistrationToken(ctx, g.entity.Owner, g.entity.Name) - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: ret, response, err = g.CreateOrganizationRegistrationToken(ctx, g.entity.Owner) - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: ret, response, err = g.enterprise.CreateRegistrationToken(ctx, g.entity.Owner) default: return nil, nil, errors.New("invalid entity type") @@ -326,7 +343,7 @@ func (g *githubClient) CreateEntityRegistrationToken(ctx context.Context) (*gith return ret, response, err } -func (g *githubClient) getOrganizationRunnerGroupIDByName(ctx context.Context, entity params.GithubEntity, rgName string) (int64, error) { +func (g *githubClient) getOrganizationRunnerGroupIDByName(ctx context.Context, entity params.ForgeEntity, rgName string) (int64, error) { opts := github.ListOrgRunnerGroupOptions{ ListOptions: github.ListOptions{ PerPage: 100, @@ -362,7 +379,7 @@ func (g *githubClient) getOrganizationRunnerGroupIDByName(ctx context.Context, e return 0, runnerErrors.NewNotFoundError("runner group %s not found", rgName) } -func (g *githubClient) getEnterpriseRunnerGroupIDByName(ctx context.Context, entity params.GithubEntity, rgName string) (int64, error) { +func (g *githubClient) getEnterpriseRunnerGroupIDByName(ctx context.Context, entity params.ForgeEntity, rgName string) (int64, error) { opts := github.ListEnterpriseRunnerGroupOptions{ ListOptions: github.ListOptions{ PerPage: 100, @@ -405,9 +422,9 @@ func (g *githubClient) GetEntityJITConfig(ctx context.Context, instance string, if pool.GitHubRunnerGroup != "" { switch g.entity.EntityType { - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: rgID, err = g.getOrganizationRunnerGroupIDByName(ctx, g.entity, pool.GitHubRunnerGroup) - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: rgID, err = g.getEnterpriseRunnerGroupIDByName(ctx, g.entity, pool.GitHubRunnerGroup) } @@ -434,11 +451,11 @@ func (g *githubClient) GetEntityJITConfig(ctx context.Context, instance string, var response *github.Response switch g.entity.EntityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: ret, response, err = g.GenerateRepoJITConfig(ctx, g.entity.Owner, g.entity.Name, &req) - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: ret, response, err = g.GenerateOrgJITConfig(ctx, g.entity.Owner, &req) - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: ret, response, err = g.enterprise.GenerateEnterpriseJITConfig(ctx, g.entity.Owner, &req) } if err != nil { @@ -476,13 +493,19 @@ func (g *githubClient) GetEntityJITConfig(ctx context.Context, instance string, func (g *githubClient) RateLimit(ctx context.Context) (*github.RateLimits, error) { limits, resp, err := g.rateLimit.Get(ctx) + if err != nil { + metrics.GithubOperationFailedCount.WithLabelValues( + "GetRateLimit", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + } if err := parseError(resp, err); err != nil { return nil, fmt.Errorf("getting rate limit: %w", err) } return limits, nil } -func (g *githubClient) GetEntity() params.GithubEntity { +func (g *githubClient) GetEntity() params.ForgeEntity { return g.entity } @@ -490,7 +513,7 @@ func (g *githubClient) GithubBaseURL() *url.URL { return g.cli.BaseURL } -func NewRateLimitClient(ctx context.Context, credentials params.GithubCredentials) (common.RateLimitClient, error) { +func NewRateLimitClient(ctx context.Context, credentials params.ForgeCredentials) (common.RateLimitClient, error) { httpClient, err := credentials.GetHTTPClient(ctx) if err != nil { return nil, errors.Wrap(err, "fetching http client") @@ -514,8 +537,36 @@ func NewRateLimitClient(ctx context.Context, credentials params.GithubCredential return cli, nil } -func Client(ctx context.Context, entity params.GithubEntity) (common.GithubClient, error) { - // func GithubClient(ctx context.Context, entity params.GithubEntity) (common.GithubClient, error) { +func withGiteaURLs(client *github.Client, apiBaseURL string) (*github.Client, error) { + if client == nil { + return nil, errors.New("client is nil") + } + + if apiBaseURL == "" { + return nil, errors.New("invalid gitea URLs") + } + + parsedBaseURL, err := url.ParseRequestURI(apiBaseURL) + if err != nil { + return nil, errors.Wrap(err, "parsing gitea base URL") + } + + if !strings.HasSuffix(parsedBaseURL.Path, "/") { + parsedBaseURL.Path += "/" + } + + if !strings.HasSuffix(parsedBaseURL.Path, "/api/v1/") { + parsedBaseURL.Path += "api/v1/" + } + + client.BaseURL = parsedBaseURL + client.UploadURL = parsedBaseURL + + return client, nil +} + +func Client(ctx context.Context, entity params.ForgeEntity) (common.GithubClient, error) { + // func GithubClient(ctx context.Context, entity params.ForgeEntity) (common.GithubClient, error) { httpClient, err := entity.Credentials.GetHTTPClient(ctx) if err != nil { return nil, errors.Wrap(err, "fetching http client") @@ -526,8 +577,14 @@ func Client(ctx context.Context, entity params.GithubEntity) (common.GithubClien "entity", entity.String(), "base_url", entity.Credentials.APIBaseURL, "upload_url", entity.Credentials.UploadBaseURL) - ghClient, err := github.NewClient(httpClient).WithEnterpriseURLs( - entity.Credentials.APIBaseURL, entity.Credentials.UploadBaseURL) + ghClient := github.NewClient(httpClient) + switch entity.Credentials.ForgeType { + case params.GithubEndpointType: + ghClient, err = ghClient.WithEnterpriseURLs(entity.Credentials.APIBaseURL, entity.Credentials.UploadBaseURL) + case params.GiteaEndpointType: + ghClient, err = withGiteaURLs(ghClient, entity.Credentials.APIBaseURL) + } + if err != nil { return nil, errors.Wrap(err, "fetching github client") } diff --git a/util/github/gitea.go b/util/github/gitea.go new file mode 100644 index 000000000..e657db483 --- /dev/null +++ b/util/github/gitea.go @@ -0,0 +1,116 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package github + +import ( + "context" + "fmt" + "net/http" + + "github.com/google/go-github/v71/github" + "github.com/pkg/errors" + + "github.com/cloudbase/garm/metrics" + "github.com/cloudbase/garm/params" +) + +type createGiteaHookOptions struct { + Type string `json:"type"` + Config map[string]string `json:"config"` + Events []string `json:"events"` + BranchFilter string `json:"branch_filter"` + Active bool `json:"active"` + AuthorizationHeader string `json:"authorization_header"` +} + +func (g *githubClient) createGiteaRepoHook(ctx context.Context, owner, name string, hook *github.Hook) (ret *github.Hook, err error) { + u := fmt.Sprintf("repos/%v/%v/hooks", owner, name) + createOpts := &createGiteaHookOptions{ + Type: "gitea", + Events: hook.Events, + Active: hook.GetActive(), + BranchFilter: "*", + Config: map[string]string{ + "content_type": hook.GetConfig().GetContentType(), + "url": hook.GetConfig().GetURL(), + "http_method": "post", + "secret": hook.GetConfig().GetSecret(), + }, + } + + req, err := g.cli.NewRequest(http.MethodPost, u, createOpts) + if err != nil { + return nil, fmt.Errorf("failed to construct request: %w", err) + } + + hook = new(github.Hook) + _, err = g.cli.Do(ctx, req, hook) + if err != nil { + return nil, fmt.Errorf("request failed for %s: %w", req.URL.String(), err) + } + return hook, nil +} + +func (g *githubClient) createGiteaOrgHook(ctx context.Context, owner string, hook *github.Hook) (ret *github.Hook, err error) { + u := fmt.Sprintf("orgs/%v/hooks", owner) + createOpts := &createGiteaHookOptions{ + Type: "gitea", + Events: hook.Events, + Active: hook.GetActive(), + BranchFilter: "*", + Config: map[string]string{ + "content_type": hook.GetConfig().GetContentType(), + "url": hook.GetConfig().GetURL(), + "http_method": "post", + "secret": hook.GetConfig().GetSecret(), + }, + } + + req, err := g.cli.NewRequest(http.MethodPost, u, createOpts) + if err != nil { + return nil, fmt.Errorf("failed to construct request: %w", err) + } + + hook = new(github.Hook) + _, err = g.cli.Do(ctx, req, hook) + if err != nil { + return nil, fmt.Errorf("request failed for %s: %w", req.URL.String(), err) + } + return hook, nil +} + +func (g *githubClient) createGiteaEntityHook(ctx context.Context, hook *github.Hook) (ret *github.Hook, err error) { + metrics.GithubOperationCount.WithLabelValues( + "CreateHook", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + defer func() { + if err != nil { + metrics.GithubOperationFailedCount.WithLabelValues( + "CreateHook", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + } + }() + switch g.entity.EntityType { + case params.ForgeEntityTypeRepository: + ret, err = g.createGiteaRepoHook(ctx, g.entity.Owner, g.entity.Name, hook) + case params.ForgeEntityTypeOrganization: + ret, err = g.createGiteaOrgHook(ctx, g.entity.Owner, hook) + default: + return nil, errors.New("invalid entity type") + } + return ret, err +} diff --git a/util/github/scalesets/token.go b/util/github/scalesets/token.go index 47aa764f0..1491b7481 100644 --- a/util/github/scalesets/token.go +++ b/util/github/scalesets/token.go @@ -36,7 +36,7 @@ func (s *ScaleSetClient) getActionServiceInfo(ctx context.Context) (params.Actio entity := s.ghCli.GetEntity() body := params.ActionsServiceAdminInfoRequest{ - URL: entity.GithubURL(), + URL: entity.ForgeURL(), RunnerEvent: "register", } diff --git a/util/logging.go b/util/logging.go index bb7b05625..4c37ed346 100644 --- a/util/logging.go +++ b/util/logging.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package util import ( diff --git a/vendor/golang.org/x/mod/LICENSE b/vendor/golang.org/x/mod/LICENSE new file mode 100644 index 000000000..6a66aea5e --- /dev/null +++ b/vendor/golang.org/x/mod/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/golang.org/x/mod/PATENTS b/vendor/golang.org/x/mod/PATENTS new file mode 100644 index 000000000..733099041 --- /dev/null +++ b/vendor/golang.org/x/mod/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/vendor/golang.org/x/mod/semver/semver.go b/vendor/golang.org/x/mod/semver/semver.go new file mode 100644 index 000000000..9a2dfd33a --- /dev/null +++ b/vendor/golang.org/x/mod/semver/semver.go @@ -0,0 +1,401 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package semver implements comparison of semantic version strings. +// In this package, semantic version strings must begin with a leading "v", +// as in "v1.0.0". +// +// The general form of a semantic version string accepted by this package is +// +// vMAJOR[.MINOR[.PATCH[-PRERELEASE][+BUILD]]] +// +// where square brackets indicate optional parts of the syntax; +// MAJOR, MINOR, and PATCH are decimal integers without extra leading zeros; +// PRERELEASE and BUILD are each a series of non-empty dot-separated identifiers +// using only alphanumeric characters and hyphens; and +// all-numeric PRERELEASE identifiers must not have leading zeros. +// +// This package follows Semantic Versioning 2.0.0 (see semver.org) +// with two exceptions. First, it requires the "v" prefix. Second, it recognizes +// vMAJOR and vMAJOR.MINOR (with no prerelease or build suffixes) +// as shorthands for vMAJOR.0.0 and vMAJOR.MINOR.0. +package semver + +import "sort" + +// parsed returns the parsed form of a semantic version string. +type parsed struct { + major string + minor string + patch string + short string + prerelease string + build string +} + +// IsValid reports whether v is a valid semantic version string. +func IsValid(v string) bool { + _, ok := parse(v) + return ok +} + +// Canonical returns the canonical formatting of the semantic version v. +// It fills in any missing .MINOR or .PATCH and discards build metadata. +// Two semantic versions compare equal only if their canonical formattings +// are identical strings. +// The canonical invalid semantic version is the empty string. +func Canonical(v string) string { + p, ok := parse(v) + if !ok { + return "" + } + if p.build != "" { + return v[:len(v)-len(p.build)] + } + if p.short != "" { + return v + p.short + } + return v +} + +// Major returns the major version prefix of the semantic version v. +// For example, Major("v2.1.0") == "v2". +// If v is an invalid semantic version string, Major returns the empty string. +func Major(v string) string { + pv, ok := parse(v) + if !ok { + return "" + } + return v[:1+len(pv.major)] +} + +// MajorMinor returns the major.minor version prefix of the semantic version v. +// For example, MajorMinor("v2.1.0") == "v2.1". +// If v is an invalid semantic version string, MajorMinor returns the empty string. +func MajorMinor(v string) string { + pv, ok := parse(v) + if !ok { + return "" + } + i := 1 + len(pv.major) + if j := i + 1 + len(pv.minor); j <= len(v) && v[i] == '.' && v[i+1:j] == pv.minor { + return v[:j] + } + return v[:i] + "." + pv.minor +} + +// Prerelease returns the prerelease suffix of the semantic version v. +// For example, Prerelease("v2.1.0-pre+meta") == "-pre". +// If v is an invalid semantic version string, Prerelease returns the empty string. +func Prerelease(v string) string { + pv, ok := parse(v) + if !ok { + return "" + } + return pv.prerelease +} + +// Build returns the build suffix of the semantic version v. +// For example, Build("v2.1.0+meta") == "+meta". +// If v is an invalid semantic version string, Build returns the empty string. +func Build(v string) string { + pv, ok := parse(v) + if !ok { + return "" + } + return pv.build +} + +// Compare returns an integer comparing two versions according to +// semantic version precedence. +// The result will be 0 if v == w, -1 if v < w, or +1 if v > w. +// +// An invalid semantic version string is considered less than a valid one. +// All invalid semantic version strings compare equal to each other. +func Compare(v, w string) int { + pv, ok1 := parse(v) + pw, ok2 := parse(w) + if !ok1 && !ok2 { + return 0 + } + if !ok1 { + return -1 + } + if !ok2 { + return +1 + } + if c := compareInt(pv.major, pw.major); c != 0 { + return c + } + if c := compareInt(pv.minor, pw.minor); c != 0 { + return c + } + if c := compareInt(pv.patch, pw.patch); c != 0 { + return c + } + return comparePrerelease(pv.prerelease, pw.prerelease) +} + +// Max canonicalizes its arguments and then returns the version string +// that compares greater. +// +// Deprecated: use [Compare] instead. In most cases, returning a canonicalized +// version is not expected or desired. +func Max(v, w string) string { + v = Canonical(v) + w = Canonical(w) + if Compare(v, w) > 0 { + return v + } + return w +} + +// ByVersion implements [sort.Interface] for sorting semantic version strings. +type ByVersion []string + +func (vs ByVersion) Len() int { return len(vs) } +func (vs ByVersion) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] } +func (vs ByVersion) Less(i, j int) bool { + cmp := Compare(vs[i], vs[j]) + if cmp != 0 { + return cmp < 0 + } + return vs[i] < vs[j] +} + +// Sort sorts a list of semantic version strings using [ByVersion]. +func Sort(list []string) { + sort.Sort(ByVersion(list)) +} + +func parse(v string) (p parsed, ok bool) { + if v == "" || v[0] != 'v' { + return + } + p.major, v, ok = parseInt(v[1:]) + if !ok { + return + } + if v == "" { + p.minor = "0" + p.patch = "0" + p.short = ".0.0" + return + } + if v[0] != '.' { + ok = false + return + } + p.minor, v, ok = parseInt(v[1:]) + if !ok { + return + } + if v == "" { + p.patch = "0" + p.short = ".0" + return + } + if v[0] != '.' { + ok = false + return + } + p.patch, v, ok = parseInt(v[1:]) + if !ok { + return + } + if len(v) > 0 && v[0] == '-' { + p.prerelease, v, ok = parsePrerelease(v) + if !ok { + return + } + } + if len(v) > 0 && v[0] == '+' { + p.build, v, ok = parseBuild(v) + if !ok { + return + } + } + if v != "" { + ok = false + return + } + ok = true + return +} + +func parseInt(v string) (t, rest string, ok bool) { + if v == "" { + return + } + if v[0] < '0' || '9' < v[0] { + return + } + i := 1 + for i < len(v) && '0' <= v[i] && v[i] <= '9' { + i++ + } + if v[0] == '0' && i != 1 { + return + } + return v[:i], v[i:], true +} + +func parsePrerelease(v string) (t, rest string, ok bool) { + // "A pre-release version MAY be denoted by appending a hyphen and + // a series of dot separated identifiers immediately following the patch version. + // Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-]. + // Identifiers MUST NOT be empty. Numeric identifiers MUST NOT include leading zeroes." + if v == "" || v[0] != '-' { + return + } + i := 1 + start := 1 + for i < len(v) && v[i] != '+' { + if !isIdentChar(v[i]) && v[i] != '.' { + return + } + if v[i] == '.' { + if start == i || isBadNum(v[start:i]) { + return + } + start = i + 1 + } + i++ + } + if start == i || isBadNum(v[start:i]) { + return + } + return v[:i], v[i:], true +} + +func parseBuild(v string) (t, rest string, ok bool) { + if v == "" || v[0] != '+' { + return + } + i := 1 + start := 1 + for i < len(v) { + if !isIdentChar(v[i]) && v[i] != '.' { + return + } + if v[i] == '.' { + if start == i { + return + } + start = i + 1 + } + i++ + } + if start == i { + return + } + return v[:i], v[i:], true +} + +func isIdentChar(c byte) bool { + return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' || c == '-' +} + +func isBadNum(v string) bool { + i := 0 + for i < len(v) && '0' <= v[i] && v[i] <= '9' { + i++ + } + return i == len(v) && i > 1 && v[0] == '0' +} + +func isNum(v string) bool { + i := 0 + for i < len(v) && '0' <= v[i] && v[i] <= '9' { + i++ + } + return i == len(v) +} + +func compareInt(x, y string) int { + if x == y { + return 0 + } + if len(x) < len(y) { + return -1 + } + if len(x) > len(y) { + return +1 + } + if x < y { + return -1 + } else { + return +1 + } +} + +func comparePrerelease(x, y string) int { + // "When major, minor, and patch are equal, a pre-release version has + // lower precedence than a normal version. + // Example: 1.0.0-alpha < 1.0.0. + // Precedence for two pre-release versions with the same major, minor, + // and patch version MUST be determined by comparing each dot separated + // identifier from left to right until a difference is found as follows: + // identifiers consisting of only digits are compared numerically and + // identifiers with letters or hyphens are compared lexically in ASCII + // sort order. Numeric identifiers always have lower precedence than + // non-numeric identifiers. A larger set of pre-release fields has a + // higher precedence than a smaller set, if all of the preceding + // identifiers are equal. + // Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < + // 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0." + if x == y { + return 0 + } + if x == "" { + return +1 + } + if y == "" { + return -1 + } + for x != "" && y != "" { + x = x[1:] // skip - or . + y = y[1:] // skip - or . + var dx, dy string + dx, x = nextIdent(x) + dy, y = nextIdent(y) + if dx != dy { + ix := isNum(dx) + iy := isNum(dy) + if ix != iy { + if ix { + return -1 + } else { + return +1 + } + } + if ix { + if len(dx) < len(dy) { + return -1 + } + if len(dx) > len(dy) { + return +1 + } + } + if dx < dy { + return -1 + } else { + return +1 + } + } + } + if x == "" { + return -1 + } else { + return +1 + } +} + +func nextIdent(x string) (dx, rest string) { + i := 0 + for i < len(x) && x[i] != '.' { + i++ + } + return x[:i], x[i:] +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 5cb70bb10..dbd42ce38 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -286,6 +286,9 @@ golang.org/x/crypto/chacha20poly1305 golang.org/x/crypto/hkdf golang.org/x/crypto/internal/alias golang.org/x/crypto/internal/poly1305 +# golang.org/x/mod v0.17.0 +## explicit; go 1.18 +golang.org/x/mod/semver # golang.org/x/net v0.40.0 ## explicit; go 1.23.0 golang.org/x/net/internal/socks diff --git a/websocket/client.go b/websocket/client.go index 5b80ba81b..707772659 100644 --- a/websocket/client.go +++ b/websocket/client.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package websocket import ( @@ -58,8 +72,6 @@ func NewClient(ctx context.Context, conn *websocket.Conn) (*Client, error) { userID: user, passwordGeneration: generation, consumer: consumer, - done: make(chan struct{}), - send: make(chan []byte, 100), }, nil } @@ -116,6 +128,8 @@ func (c *Client) Start() error { defer c.mux.Unlock() c.running = true + c.send = make(chan []byte, 100) + c.done = make(chan struct{}) go c.runWatcher() go c.clientReader() diff --git a/websocket/websocket.go b/websocket/websocket.go index 578204499..e59eb70e8 100644 --- a/websocket/websocket.go +++ b/websocket/websocket.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package websocket import ( @@ -28,14 +41,15 @@ type Hub struct { // Inbound messages from the clients. broadcast chan []byte - mux sync.Mutex - once sync.Once + mux sync.Mutex + running bool + once sync.Once } func (h *Hub) run() { - defer func() { - close(h.closed) - }() + defer close(h.closed) + defer h.Stop() + for { select { case <-h.quit: @@ -59,8 +73,7 @@ func (h *Hub) run() { for _, id := range staleClients { if client, ok := h.clients[id]; ok { if client != nil { - client.conn.Close() - close(client.send) + client.Stop() } delete(h.clients, id) } @@ -105,6 +118,13 @@ func (h *Hub) Unregister(client *Client) error { } func (h *Hub) Write(msg []byte) (int, error) { + h.mux.Lock() + if !h.running { + h.mux.Unlock() + return 0, fmt.Errorf("websocket writer is not running") + } + h.mux.Unlock() + tmp := make([]byte, len(msg)) copy(tmp, msg) timer := time.NewTimer(5 * time.Second) @@ -118,6 +138,15 @@ func (h *Hub) Write(msg []byte) (int, error) { } func (h *Hub) Start() error { + h.mux.Lock() + defer h.mux.Unlock() + + if h.running { + return nil + } + + h.running = true + go h.run() return nil } @@ -130,11 +159,22 @@ func (h *Hub) Close() error { } func (h *Hub) Stop() error { + h.mux.Lock() + defer h.mux.Unlock() + + if !h.running { + return nil + } + + h.running = false h.Close() return h.Wait() } func (h *Hub) Wait() error { + if !h.running { + return nil + } timer := time.NewTimer(60 * time.Second) defer timer.Stop() select { diff --git a/workers/cache/cache.go b/workers/cache/cache.go index 13400a3ab..8f53cb671 100644 --- a/workers/cache/cache.go +++ b/workers/cache/cache.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package cache import ( @@ -138,7 +151,7 @@ func (w *Worker) loadAllInstances() error { return nil } -func (w *Worker) loadAllCredentials() error { +func (w *Worker) loadAllGithubCredentials() error { creds, err := w.store.ListGithubCredentials(w.ctx) if err != nil { return fmt.Errorf("listing github credentials: %w", err) @@ -150,6 +163,18 @@ func (w *Worker) loadAllCredentials() error { return nil } +func (w *Worker) loadAllGiteaCredentials() error { + creds, err := w.store.ListGiteaCredentials(w.ctx) + if err != nil { + return fmt.Errorf("listing gitea credentials: %w", err) + } + + for _, cred := range creds { + cache.SetGiteaCredentials(cred) + } + return nil +} + func (w *Worker) waitForErrorGroupOrContextCancelled(g *errgroup.Group) error { if g == nil { return nil @@ -183,22 +208,29 @@ func (w *Worker) Start() error { g, _ := errgroup.WithContext(w.ctx) g.Go(func() error { - if err := w.loadAllEntities(); err != nil { - return fmt.Errorf("loading all entities: %w", err) + if err := w.loadAllGithubCredentials(); err != nil { + return fmt.Errorf("loading all github credentials: %w", err) } return nil }) g.Go(func() error { - if err := w.loadAllInstances(); err != nil { - return fmt.Errorf("loading all instances: %w", err) + if err := w.loadAllGiteaCredentials(); err != nil { + return fmt.Errorf("loading all gitea credentials: %w", err) } return nil }) g.Go(func() error { - if err := w.loadAllCredentials(); err != nil { - return fmt.Errorf("loading all credentials: %w", err) + if err := w.loadAllEntities(); err != nil { + return fmt.Errorf("loading all entities: %w", err) + } + return nil + }) + + g.Go(func() error { + if err := w.loadAllInstances(); err != nil { + return fmt.Errorf("loading all instances: %w", err) } return nil }) @@ -262,7 +294,7 @@ func (w *Worker) handleEntityEvent(entityGetter params.EntityGetter, op common.O w.toolsWorkes[entity.ID] = worker } else if hasOld { // probably an update operation - if old.Credentials.ID != entity.Credentials.ID { + if old.Credentials.GetID() != entity.Credentials.GetID() { worker.Reset() } } @@ -360,15 +392,23 @@ func (w *Worker) handleInstanceEvent(event common.ChangePayload) { } func (w *Worker) handleCredentialsEvent(event common.ChangePayload) { - credentials, ok := event.Payload.(params.GithubCredentials) + credentials, ok := event.Payload.(params.ForgeCredentials) if !ok { slog.DebugContext(w.ctx, "invalid payload type for credentials event", "payload", event.Payload) return } switch event.Operation { case common.CreateOperation, common.UpdateOperation: - cache.SetGithubCredentials(credentials) - entities := cache.GetEntitiesUsingGredentials(credentials.ID) + switch credentials.ForgeType { + case params.GithubEndpointType: + cache.SetGithubCredentials(credentials) + case params.GiteaEndpointType: + cache.SetGiteaCredentials(credentials) + default: + slog.DebugContext(w.ctx, "invalid credentials type", "credentials_type", credentials.ForgeType) + return + } + entities := cache.GetEntitiesUsingCredentials(credentials) for _, entity := range entities { worker, ok := w.toolsWorkes[entity.ID] if ok { @@ -395,7 +435,7 @@ func (w *Worker) handleEvent(event common.ChangePayload) { w.handleOrgEvent(event) case common.EnterpriseEntityType: w.handleEnterpriseEvent(event) - case common.GithubCredentialsEntityType: + case common.GithubCredentialsEntityType, common.GiteaCredentialsEntityType: w.handleCredentialsEvent(event) default: slog.DebugContext(w.ctx, "unknown entity type", "entity_type", event.EntityType) diff --git a/workers/cache/gitea_tools.go b/workers/cache/gitea_tools.go new file mode 100644 index 000000000..5d09ccb34 --- /dev/null +++ b/workers/cache/gitea_tools.go @@ -0,0 +1,188 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cache + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "golang.org/x/mod/semver" + + commonParams "github.com/cloudbase/garm-provider-common/params" +) + +const ( + // GiteaRunnerReleasesURL is the public API URL that returns a json of all Gitea runner releases. + // By default it returns the last 10 releases, which is enough for our needs. + GiteaRunnerReleasesURL = "https://gitea.com/api/v1/repos/gitea/act_runner/releases" + // GiteaRunnerMinimumVersion is the minimum version we need in order to support ephemeral runners. + GiteaRunnerMinimumVersion = "v0.2.12" +) + +var githubArchMapping = map[string]string{ + "x86_64": "x64", + "amd64": "x64", + "armv7l": "arm", + "aarch64": "arm64", + "x64": "x64", + "arm": "arm", + "arm64": "arm64", +} + +var nightlyActRunner = GiteaEntityTool{ + TagName: "nightly", + Name: "nightly", + TarballURL: "https://gitea.com/gitea/act_runner/archive/main.tar.gz", + Assets: []GiteaToolsAssets{ + { + Name: "act_runner-nightly-linux-amd64.xz", + DownloadURL: "https://dl.gitea.com/act_runner/nightly/act_runner-nightly-linux-amd64.xz", + }, + { + Name: "act_runner-nightly-linux-arm64.xz", + DownloadURL: "https://dl.gitea.com/act_runner/nightly/act_runner-nightly-linux-arm64.xz", + }, + { + Name: "act_runner-nightly-windows-amd64.exe.xz", + DownloadURL: "https://dl.gitea.com/act_runner/nightly/act_runner-nightly-windows-amd64.exe.xz", + }, + }, +} + +type GiteaToolsAssets struct { + ID uint `json:"id"` + Name string `json:"name"` + Size uint `json:"size"` + DownloadCount uint `json:"download_count"` + CreatedAt time.Time `json:"created_at"` + UUID string `json:"uuid"` + DownloadURL string `json:"browser_download_url"` +} + +func (g GiteaToolsAssets) GetOS() (*string, error) { + if g.Name == "" { + return nil, fmt.Errorf("gitea tools name is empty") + } + + parts := strings.SplitN(g.Name, "-", 4) + if len(parts) != 4 { + return nil, fmt.Errorf("could not parse asset name") + } + + os := parts[2] + return &os, nil +} + +func (g GiteaToolsAssets) GetArch() (*string, error) { + if g.Name == "" { + return nil, fmt.Errorf("gitea tools name is empty") + } + + parts := strings.SplitN(g.Name, "-", 4) + if len(parts) != 4 { + return nil, fmt.Errorf("could not parse asset name") + } + + archParts := strings.SplitN(parts[3], ".", 2) + if len(archParts) == 0 { + return nil, fmt.Errorf("unexpected asset name format") + } + arch := githubArchMapping[archParts[0]] + if arch == "" { + return nil, fmt.Errorf("could not find arch for %s", archParts[0]) + } + return &arch, nil +} + +type GiteaEntityTool struct { + // TagName is the semver version of the release. + TagName string `json:"tag_name"` + Name string `json:"name"` + TarballURL string `json:"tarball_url"` + Assets []GiteaToolsAssets `json:"assets"` +} + +type GiteaEntityTools []GiteaEntityTool + +func (g GiteaEntityTools) GetLatestVersion() string { + if len(g) == 0 { + return "" + } + return g[0].TagName +} + +func (g GiteaEntityTools) MinimumVersion() (GiteaEntityTool, bool) { + if len(g) == 0 { + return GiteaEntityTool{}, false + } + for _, tool := range g { + if semver.Compare(tool.TagName, GiteaRunnerMinimumVersion) >= 0 { + return tool, true + } + } + return GiteaEntityTool{}, false +} + +func getTools() ([]commonParams.RunnerApplicationDownload, error) { + resp, err := http.Get(GiteaRunnerReleasesURL) + if err != nil { + return nil, err + } + defer resp.Body.Close() + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var tools GiteaEntityTools + err = json.Unmarshal(data, &tools) + if err != nil { + return nil, err + } + + if len(tools) == 0 { + return nil, fmt.Errorf("no tools found") + } + + latest, ok := tools.MinimumVersion() + if !ok { + latest = nightlyActRunner + } + + ret := []commonParams.RunnerApplicationDownload{} + + for _, asset := range latest.Assets { + arch, err := asset.GetArch() + if err != nil { + return nil, fmt.Errorf("getting arch: %w", err) + } + os, err := asset.GetOS() + if err != nil { + return nil, fmt.Errorf("getting os: %w", err) + } + ret = append(ret, commonParams.RunnerApplicationDownload{ + OS: os, + Architecture: arch, + DownloadURL: &asset.DownloadURL, + Filename: &asset.Name, + }) + } + + return ret, nil +} diff --git a/workers/cache/tool_cache.go b/workers/cache/tool_cache.go index 6133580d3..2e91bf504 100644 --- a/workers/cache/tool_cache.go +++ b/workers/cache/tool_cache.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package cache import ( @@ -16,7 +30,7 @@ import ( "github.com/cloudbase/garm/util/github" ) -func newToolsUpdater(ctx context.Context, entity params.GithubEntity) *toolsUpdater { +func newToolsUpdater(ctx context.Context, entity params.ForgeEntity) *toolsUpdater { return &toolsUpdater{ ctx: ctx, entity: entity, @@ -27,7 +41,7 @@ func newToolsUpdater(ctx context.Context, entity params.GithubEntity) *toolsUpda type toolsUpdater struct { ctx context.Context - entity params.GithubEntity + entity params.ForgeEntity tools []commonParams.RunnerApplicationDownload lastUpdate time.Time @@ -49,7 +63,14 @@ func (t *toolsUpdater) Start() error { t.running = true t.quit = make(chan struct{}) - go t.loop() + slog.DebugContext(t.ctx, "starting tools updater", "entity", t.entity.String(), "forge_type", t.entity.Credentials.ForgeType) + + switch t.entity.Credentials.ForgeType { + case params.GithubEndpointType: + go t.loop() + case params.GiteaEndpointType: + go t.giteaUpdateLoop() + } return nil } @@ -68,7 +89,7 @@ func (t *toolsUpdater) Stop() error { } func (t *toolsUpdater) updateTools() error { - slog.DebugContext(t.ctx, "updating tools", "entity", t.entity.String()) + slog.DebugContext(t.ctx, "updating tools", "entity", t.entity.String(), "forge_type", t.entity.Credentials.ForgeType) entity, ok := cache.GetEntity(t.entity.ID) if !ok { return fmt.Errorf("getting entity from cache: %s", t.entity.ID) @@ -98,12 +119,69 @@ func (t *toolsUpdater) Reset() { return } + if t.entity.Credentials.ForgeType == params.GiteaEndpointType { + // no need to reset the gitea tools updater when credentials + // are updated. + return + } + if t.reset != nil { close(t.reset) t.reset = nil } } +func (t *toolsUpdater) sleepWithCancel(sleepTime time.Duration) (canceled bool) { + ticker := time.NewTicker(sleepTime) + defer ticker.Stop() + + select { + case <-ticker.C: + return false + case <-t.quit: + case <-t.ctx.Done(): + } + return true +} + +// giteaUpdateLoop updates tools for gitea. The act runner can be downloaded +// without a token, unlike the github tools, which for GHES require a token. +func (t *toolsUpdater) giteaUpdateLoop() { + defer t.Stop() + + // add some jitter. When spinning up multiple entities, we add + // jitter to prevent stampeeding herd. + randInt, err := rand.Int(rand.Reader, big.NewInt(3000)) + if err != nil { + randInt = big.NewInt(0) + } + t.sleepWithCancel(time.Duration(randInt.Int64()) * time.Millisecond) + tools, err := getTools() + if err == nil { + cache.SetGithubToolsCache(t.entity, tools) + } + + // Once every 3 hours should be enough. Tools don't expire. + ticker := time.NewTicker(3 * time.Hour) + + for { + select { + case <-t.quit: + slog.DebugContext(t.ctx, "stopping tools updater") + return + case <-t.ctx.Done(): + return + case <-ticker.C: + tools, err := getTools() + if err != nil { + slog.DebugContext(t.ctx, "failed to update gitea tools", "error", err) + continue + } + cache.SetGithubToolsCache(t.entity, tools) + } + } +} + func (t *toolsUpdater) loop() { defer t.Stop() @@ -113,7 +191,7 @@ func (t *toolsUpdater) loop() { if err != nil { randInt = big.NewInt(0) } - time.Sleep(time.Duration(randInt.Int64()) * time.Millisecond) + t.sleepWithCancel(time.Duration(randInt.Int64()) * time.Millisecond) var resetTime time.Time now := time.Now().UTC() diff --git a/workers/common/interfaces.go b/workers/common/interfaces.go index 4791a500f..a04f16a6f 100644 --- a/workers/common/interfaces.go +++ b/workers/common/interfaces.go @@ -1,3 +1,17 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + package common import ( diff --git a/workers/entity/controller.go b/workers/entity/controller.go index db353f0ea..996181943 100644 --- a/workers/entity/controller.go +++ b/workers/entity/controller.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package entity import ( @@ -215,6 +228,7 @@ func (c *Controller) Stop() error { c.running = false close(c.quit) c.consumer.Close() + slog.DebugContext(c.ctx, "stopped entity controller", "entity", c.consumerID) return nil } diff --git a/workers/entity/controller_watcher.go b/workers/entity/controller_watcher.go index ace637029..6bd3e1737 100644 --- a/workers/entity/controller_watcher.go +++ b/workers/entity/controller_watcher.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package entity import ( diff --git a/workers/entity/util.go b/workers/entity/util.go index 4912bebad..2216c3267 100644 --- a/workers/entity/util.go +++ b/workers/entity/util.go @@ -1,6 +1,21 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package entity import ( + "strings" + "golang.org/x/sync/errgroup" dbCommon "github.com/cloudbase/garm/database/common" @@ -8,6 +23,13 @@ import ( "github.com/cloudbase/garm/params" ) +const ( + // These are duplicated until we decide if we move the pool manager to the new + // worker flow. + poolIDLabelprefix = "runner-pool-id:" + controllerLabelPrefix = "runner-controller-id:" +) + func composeControllerWatcherFilters() dbCommon.PayloadFilterFunc { return watcher.WithAll( watcher.WithAny( @@ -22,7 +44,7 @@ func composeControllerWatcherFilters() dbCommon.PayloadFilterFunc { ) } -func composeWorkerWatcherFilters(entity params.GithubEntity) dbCommon.PayloadFilterFunc { +func composeWorkerWatcherFilters(entity params.ForgeEntity) dbCommon.PayloadFilterFunc { return watcher.WithAny( watcher.WithAll( watcher.WithEntityFilter(entity), @@ -30,7 +52,7 @@ func composeWorkerWatcherFilters(entity params.GithubEntity) dbCommon.PayloadFil ), // Watch for credentials updates. watcher.WithAll( - watcher.WithGithubCredentialsFilter(entity.Credentials), + watcher.WithForgeCredentialsFilter(entity.Credentials), watcher.WithOperationTypeFilter(dbCommon.UpdateOperation), ), ) @@ -56,3 +78,12 @@ func (c *Controller) waitForErrorGroupOrContextCancelled(g *errgroup.Group) erro return nil } } + +func poolIDFromLabels(runner params.RunnerReference) string { + for _, lbl := range runner.Labels { + if strings.HasPrefix(lbl.Name, poolIDLabelprefix) { + return lbl.Name[len(poolIDLabelprefix):] + } + } + return "" +} diff --git a/workers/entity/worker.go b/workers/entity/worker.go index 8aebb747c..583ab2c8e 100644 --- a/workers/entity/worker.go +++ b/workers/entity/worker.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package entity import ( @@ -5,17 +18,23 @@ import ( "fmt" "log/slog" "sync" + "time" + "golang.org/x/sync/errgroup" + + "github.com/cloudbase/garm/cache" dbCommon "github.com/cloudbase/garm/database/common" "github.com/cloudbase/garm/database/watcher" "github.com/cloudbase/garm/params" "github.com/cloudbase/garm/runner/common" garmUtil "github.com/cloudbase/garm/util" + "github.com/cloudbase/garm/util/github" + "github.com/cloudbase/garm/util/github/scalesets" "github.com/cloudbase/garm/workers/scaleset" ) -func NewWorker(ctx context.Context, store dbCommon.Store, entity params.GithubEntity, providers map[string]common.Provider) (*Worker, error) { - consumerID := fmt.Sprintf("entity-worker-%s", entity.String()) +func NewWorker(ctx context.Context, store dbCommon.Store, entity params.ForgeEntity, providers map[string]common.Provider) (*Worker, error) { + consumerID := fmt.Sprintf("entity-worker-%s", entity.ID) ctx = garmUtil.WithSlogContext( ctx, @@ -36,8 +55,9 @@ type Worker struct { consumer dbCommon.Consumer store dbCommon.Store + ghCli common.GithubClient - Entity params.GithubEntity + Entity params.ForgeEntity providers map[string]common.Provider scaleSetController *scaleset.Controller @@ -63,6 +83,7 @@ func (w *Worker) Stop() error { w.running = false close(w.quit) w.consumer.Close() + slog.DebugContext(w.ctx, "entity worker stopped", "entity", w.consumerID) return nil } @@ -71,6 +92,13 @@ func (w *Worker) Start() (err error) { w.mux.Lock() defer w.mux.Unlock() + ghCli, err := github.Client(w.ctx, w.Entity) + if err != nil { + return fmt.Errorf("creating github client: %w", err) + } + w.ghCli = ghCli + cache.SetGithubClient(w.Entity.ID, ghCli) + scaleSetController, err := scaleset.NewController(w.ctx, w.store, w.Entity, w.providers) if err != nil { return fmt.Errorf("creating scale set controller: %w", err) @@ -100,16 +128,110 @@ func (w *Worker) Start() (err error) { w.quit = make(chan struct{}) go w.loop() + go w.consolidateRunnerLoop() + return nil +} + +// consolidateRunnerState will list all runners on GitHub for this entity, sort by +// pool or scale set and pass those runners to the appropriate controller (pools or scale sets). +// The controller will then pass along to their respective workers the list of runners +// they should be responsible for. The workers will then cross check the current state +// from github with their local state and reconcile any differences. This cleans up +// any runners that have been removed out of band in either the provider or github. +func (w *Worker) consolidateRunnerState() error { + scaleSetCli, err := scalesets.NewClient(w.ghCli) + if err != nil { + return fmt.Errorf("creating scaleset client: %w", err) + } + // Client is scoped to the current entity. Only runners in a repo/org/enterprise + // will be listed. + runners, err := scaleSetCli.ListAllRunners(w.ctx) + if err != nil { + return fmt.Errorf("listing runners: %w", err) + } + + byPoolID := make(map[string][]params.RunnerReference) + byScaleSetID := make(map[int][]params.RunnerReference) + for _, runner := range runners.RunnerReferences { + if runner.RunnerScaleSetID != 0 { + byScaleSetID[runner.RunnerScaleSetID] = append(byScaleSetID[runner.RunnerScaleSetID], runner) + } else { + poolID := poolIDFromLabels(runner) + if poolID == "" { + continue + } + byPoolID[poolID] = append(byPoolID[poolID], runner) + } + } + + g, ctx := errgroup.WithContext(w.ctx) + g.Go(func() error { + slog.DebugContext(ctx, "consolidating scale set runners", "entity", w.Entity.String(), "runners", runners) + if err := w.scaleSetController.ConsolidateRunnerState(byScaleSetID); err != nil { + return fmt.Errorf("consolidating runners for scale set: %w", err) + } + return nil + }) + + if err := w.waitForErrorGroupOrContextCancelled(g); err != nil { + return fmt.Errorf("waiting for error group: %w", err) + } return nil } +func (w *Worker) waitForErrorGroupOrContextCancelled(g *errgroup.Group) error { + if g == nil { + return nil + } + + done := make(chan error, 1) + go func() { + waitErr := g.Wait() + done <- waitErr + }() + + select { + case err := <-done: + return err + case <-w.ctx.Done(): + return w.ctx.Err() + case <-w.quit: + return nil + } +} + +func (w *Worker) consolidateRunnerLoop() { + ticker := time.NewTicker(common.PoolReapTimeoutInterval) + defer ticker.Stop() + + for { + select { + case _, ok := <-ticker.C: + if !ok { + slog.InfoContext(w.ctx, "consolidate ticker closed") + return + } + if err := w.consolidateRunnerState(); err != nil { + if err := w.store.AddEntityEvent(w.ctx, w.Entity, params.StatusEvent, params.EventError, fmt.Sprintf("failed to consolidate runner state: %q", err.Error()), 30); err != nil { + slog.With(slog.Any("error", err)).Error("failed to add entity event") + } + slog.With(slog.Any("error", err)).Error("failed to consolidate runner state") + } + case <-w.ctx.Done(): + return + case <-w.quit: + return + } + } +} + func (w *Worker) loop() { defer w.Stop() for { select { case payload := <-w.consumer.Watch(): slog.InfoContext(w.ctx, "received payload") - go w.handleWorkerWatcherEvent(payload) + w.handleWorkerWatcherEvent(payload) case <-w.ctx.Done(): return case <-w.quit: diff --git a/workers/entity/worker_watcher.go b/workers/entity/worker_watcher.go index 4ce83ddff..9acfbc60c 100644 --- a/workers/entity/worker_watcher.go +++ b/workers/entity/worker_watcher.go @@ -1,10 +1,25 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package entity import ( "log/slog" + "github.com/cloudbase/garm/cache" dbCommon "github.com/cloudbase/garm/database/common" "github.com/cloudbase/garm/params" + "github.com/cloudbase/garm/util/github" ) func (w *Worker) handleWorkerWatcherEvent(event dbCommon.ChangePayload) { @@ -15,7 +30,7 @@ func (w *Worker) handleWorkerWatcherEvent(event dbCommon.ChangePayload) { case entityType: w.handleEntityEventPayload(event) return - case dbCommon.GithubCredentialsEntityType: + case dbCommon.GithubCredentialsEntityType, dbCommon.GiteaCredentialsEntityType: slog.DebugContext(w.ctx, "got github credentials payload event") w.handleEntityCredentialsEventPayload(event) default: @@ -42,10 +57,17 @@ func (w *Worker) handleEntityEventPayload(event dbCommon.ChangePayload) { defer w.mux.Unlock() credentials := entity.Credentials - if w.Entity.Credentials.ID != credentials.ID { + if w.Entity.Credentials.GetID() != credentials.GetID() { // credentials were swapped on the entity. We need to recompose the watcher // filters. w.consumer.SetFilters(composeWorkerWatcherFilters(entity)) + ghCli, err := github.Client(w.ctx, entity) + if err != nil { + slog.ErrorContext(w.ctx, "creating github client", "entity_id", entity.ID, "error", err) + return + } + w.ghCli = ghCli + cache.SetGithubClient(entity.ID, ghCli) } w.Entity = entity default: @@ -54,24 +76,41 @@ func (w *Worker) handleEntityEventPayload(event dbCommon.ChangePayload) { } func (w *Worker) handleEntityCredentialsEventPayload(event dbCommon.ChangePayload) { - credentials, ok := event.Payload.(params.GithubCredentials) + var creds params.ForgeCredentials + var ok bool + switch event.EntityType { + case dbCommon.GithubCredentialsEntityType, dbCommon.GiteaCredentialsEntityType: + creds, ok = event.Payload.(params.ForgeCredentials) + default: + slog.ErrorContext(w.ctx, "invalid entity type", "entity_type", event.EntityType) + return + } if !ok { slog.ErrorContext(w.ctx, "invalid payload for entity type", "entity_type", event.EntityType, "payload", event.Payload) return } + credentials := creds + switch event.Operation { case dbCommon.UpdateOperation: slog.DebugContext(w.ctx, "got delete operation") w.mux.Lock() defer w.mux.Unlock() - if w.Entity.Credentials.ID != credentials.ID { + if w.Entity.Credentials.GetID() != credentials.GetID() { // The channel is buffered. We may get an old update. If credentials get updated // immediately after they are swapped on the entity, we may still get an update // pushed to the channel before the filters are swapped. We can ignore the update. return } w.Entity.Credentials = credentials + ghCli, err := github.Client(w.ctx, w.Entity) + if err != nil { + slog.ErrorContext(w.ctx, "creating github client", "entity_id", w.Entity.ID, "error", err) + return + } + w.ghCli = ghCli + cache.SetGithubClient(w.Entity.ID, ghCli) default: slog.ErrorContext(w.ctx, "invalid operation type", "operation_type", event.Operation) } diff --git a/workers/provider/errors.go b/workers/provider/errors.go index 40cfc9a81..7c9247dc1 100644 --- a/workers/provider/errors.go +++ b/workers/provider/errors.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package provider import "fmt" diff --git a/workers/provider/instance_manager.go b/workers/provider/instance_manager.go index 37680cd00..84e5bcca6 100644 --- a/workers/provider/instance_manager.go +++ b/workers/provider/instance_manager.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package provider import ( @@ -52,7 +65,7 @@ type instanceManager struct { helper providerHelper scaleSet params.ScaleSet - scaleSetEntity params.GithubEntity + scaleSetEntity params.ForgeEntity deleteBackoff time.Duration @@ -120,14 +133,14 @@ func (i *instanceManager) incrementBackOff() { } } -func (i *instanceManager) getEntity() (params.GithubEntity, error) { +func (i *instanceManager) getEntity() (params.ForgeEntity, error) { entity, err := i.scaleSet.GetEntity() if err != nil { - return params.GithubEntity{}, fmt.Errorf("getting entity: %w", err) + return params.ForgeEntity{}, fmt.Errorf("getting entity: %w", err) } ghEntity, err := i.helper.GetGithubEntity(entity) if err != nil { - return params.GithubEntity{}, fmt.Errorf("getting entity: %w", err) + return params.ForgeEntity{}, fmt.Errorf("getting entity: %w", err) } return ghEntity, nil } @@ -144,19 +157,19 @@ func (i *instanceManager) handleCreateInstanceInProvider(instance params.Instanc } token, err := i.helper.InstanceTokenGetter().NewInstanceJWTToken( - instance, entity.String(), entity.EntityType, i.scaleSet.RunnerBootstrapTimeout) + instance, entity, entity.EntityType, i.scaleSet.RunnerBootstrapTimeout) if err != nil { return fmt.Errorf("creating instance token: %w", err) } - tools, ok := cache.GetGithubToolsCache(entity.ID) - if !ok { - return fmt.Errorf("tools not found in cache for entity %s", entity.String()) + tools, err := cache.GetGithubToolsCache(entity.ID) + if err != nil { + return fmt.Errorf("tools not found in cache for entity %s: %w", entity.String(), err) } bootstrapArgs := commonParams.BootstrapInstance{ Name: instance.Name, Tools: tools, - RepoURL: entity.GithubURL(), + RepoURL: entity.ForgeURL(), MetadataURL: instance.MetadataURL, CallbackURL: instance.CallbackURL, InstanceToken: token, diff --git a/workers/provider/provider.go b/workers/provider/provider.go index ffc5183d3..78e50955e 100644 --- a/workers/provider/provider.go +++ b/workers/provider/provider.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package provider import ( @@ -6,8 +19,6 @@ import ( "log/slog" "sync" - "golang.org/x/sync/errgroup" - commonParams "github.com/cloudbase/garm-provider-common/params" "github.com/cloudbase/garm/auth" dbCommon "github.com/cloudbase/garm/database/common" @@ -133,24 +144,12 @@ func (p *Provider) Start() error { return nil } - g, _ := errgroup.WithContext(p.ctx) - - g.Go(func() error { - if err := p.loadAllScaleSets(); err != nil { - return fmt.Errorf("loading all scale sets: %w", err) - } - return nil - }) - - g.Go(func() error { - if err := p.loadAllRunners(); err != nil { - return fmt.Errorf("loading all runners: %w", err) - } - return nil - }) + if err := p.loadAllScaleSets(); err != nil { + return fmt.Errorf("loading all scale sets: %w", err) + } - if err := p.waitForErrorGroupOrContextCancelled(g); err != nil { - return fmt.Errorf("waiting for error group: %w", err) + if err := p.loadAllRunners(); err != nil { + return fmt.Errorf("loading all runners: %w", err) } consumer, err := watcher.RegisterConsumer( diff --git a/workers/provider/provider_helper.go b/workers/provider/provider_helper.go index 6a53bab38..136947940 100644 --- a/workers/provider/provider_helper.go +++ b/workers/provider/provider_helper.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package provider import ( @@ -14,7 +27,7 @@ type providerHelper interface { InstanceTokenGetter() auth.InstanceTokenGetter updateArgsFromProviderInstance(instanceName string, providerInstance commonParams.ProviderInstance) (params.Instance, error) GetControllerInfo() (params.ControllerInfo, error) - GetGithubEntity(entity params.GithubEntity) (params.GithubEntity, error) + GetGithubEntity(entity params.ForgeEntity) (params.ForgeEntity, error) } func (p *Provider) updateArgsFromProviderInstance(instanceName string, providerInstance commonParams.ProviderInstance) (params.Instance, error) { @@ -71,10 +84,10 @@ func (p *Provider) InstanceTokenGetter() auth.InstanceTokenGetter { return p.tokenGetter } -func (p *Provider) GetGithubEntity(entity params.GithubEntity) (params.GithubEntity, error) { - ghEntity, err := p.store.GetGithubEntity(p.ctx, entity.EntityType, entity.ID) +func (p *Provider) GetGithubEntity(entity params.ForgeEntity) (params.ForgeEntity, error) { + ghEntity, err := p.store.GetForgeEntity(p.ctx, entity.EntityType, entity.ID) if err != nil { - return params.GithubEntity{}, fmt.Errorf("getting github entity: %w", err) + return params.ForgeEntity{}, fmt.Errorf("getting github entity: %w", err) } return ghEntity, nil diff --git a/workers/provider/util.go b/workers/provider/util.go index ca2626c0d..cf27d14f4 100644 --- a/workers/provider/util.go +++ b/workers/provider/util.go @@ -1,8 +1,19 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package provider import ( - "golang.org/x/sync/errgroup" - dbCommon "github.com/cloudbase/garm/database/common" "github.com/cloudbase/garm/database/watcher" ) @@ -13,24 +24,3 @@ func composeProviderWatcher() dbCommon.PayloadFilterFunc { watcher.WithEntityTypeFilter(dbCommon.ScaleSetEntityType), ) } - -func (p *Provider) waitForErrorGroupOrContextCancelled(g *errgroup.Group) error { - if g == nil { - return nil - } - - done := make(chan error, 1) - go func() { - waitErr := g.Wait() - done <- waitErr - }() - - select { - case err := <-done: - return err - case <-p.ctx.Done(): - return p.ctx.Err() - case <-p.quit: - return nil - } -} diff --git a/workers/scaleset/controller.go b/workers/scaleset/controller.go index 3b4287c24..63112f43f 100644 --- a/workers/scaleset/controller.go +++ b/workers/scaleset/controller.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package scaleset import ( @@ -5,7 +18,6 @@ import ( "fmt" "log/slog" "sync" - "time" "golang.org/x/sync/errgroup" @@ -14,19 +26,10 @@ import ( "github.com/cloudbase/garm/params" "github.com/cloudbase/garm/runner/common" garmUtil "github.com/cloudbase/garm/util" - "github.com/cloudbase/garm/util/github" - "github.com/cloudbase/garm/util/github/scalesets" ) -const ( - // These are duplicated until we decide if we move the pool manager to the new - // worker flow. - poolIDLabelprefix = "runner-pool-id:" - controllerLabelPrefix = "runner-controller-id:" -) - -func NewController(ctx context.Context, store dbCommon.Store, entity params.GithubEntity, providers map[string]common.Provider) (*Controller, error) { - consumerID := fmt.Sprintf("scaleset-controller-%s", entity.String()) +func NewController(ctx context.Context, store dbCommon.Store, entity params.ForgeEntity, providers map[string]common.Provider) (*Controller, error) { + consumerID := fmt.Sprintf("scaleset-controller-%s", entity.ID) ctx = garmUtil.WithSlogContext( ctx, @@ -67,20 +70,18 @@ type Controller struct { ScaleSets map[uint]*scaleSet - Entity params.GithubEntity + Entity params.ForgeEntity consumer dbCommon.Consumer store dbCommon.Store providers map[string]common.Provider - ghCli common.GithubClient - mux sync.Mutex running bool quit chan struct{} } -func (c *Controller) loadAllScaleSets(cli common.GithubClient) error { +func (c *Controller) loadAllScaleSets() error { scaleSets, err := c.store.ListEntityScaleSets(c.ctx, c.Entity) if err != nil { return fmt.Errorf("listing scale sets: %w", err) @@ -88,7 +89,7 @@ func (c *Controller) loadAllScaleSets(cli common.GithubClient) error { for _, sSet := range scaleSets { slog.DebugContext(c.ctx, "loading scale set", "scale_set", sSet.ID) - if err := c.handleScaleSetCreateOperation(sSet, cli); err != nil { + if err := c.handleScaleSetCreateOperation(sSet); err != nil { slog.With(slog.Any("error", err)).ErrorContext(c.ctx, "failed to handle scale set create operation") continue } @@ -105,14 +106,16 @@ func (c *Controller) Start() (err error) { } c.mux.Unlock() - ghCli, err := github.Client(c.ctx, c.Entity) + forgeType, err := c.Entity.GetForgeType() if err != nil { - return fmt.Errorf("creating github client: %w", err) + return fmt.Errorf("getting forge type: %w", err) } - - slog.DebugContext(c.ctx, "loaging scale sets", "entity", c.Entity.String()) - if err := c.loadAllScaleSets(ghCli); err != nil { - return fmt.Errorf("loading all scale sets: %w", err) + if forgeType == params.GithubEndpointType { + // scale sets are only available in Github + slog.DebugContext(c.ctx, "loaging scale sets", "entity", c.Entity.String()) + if err := c.loadAllScaleSets(); err != nil { + return fmt.Errorf("loading all scale sets: %w", err) + } } consumer, err := watcher.RegisterConsumer( @@ -120,11 +123,10 @@ func (c *Controller) Start() (err error) { composeControllerWatcherFilters(c.Entity), ) if err != nil { - return fmt.Errorf("registering consumer: %w", err) + return fmt.Errorf("registering consumer %q: %w", c.consumerID, err) } c.mux.Lock() - c.ghCli = ghCli c.consumer = consumer c.running = true c.quit = make(chan struct{}) @@ -155,43 +157,15 @@ func (c *Controller) Stop() error { c.running = false close(c.quit) c.consumer.Close() - + slog.DebugContext(c.ctx, "stopped scale set controller", "entity", c.Entity.String()) return nil } -// consolidateRunnerState will list all runners on GitHub for this entity, sort by -// pool or scale set and pass those runners to the appropriate worker. The worker will -// then have the responsibility to cross check the runners from github with what it -// knows should be true from the database. Any inconsistency needs to be handled. -// If we have an offline runner in github but no database entry for it, we remove the -// runner from github. If we have a runner that is active in the provider but does not -// exist in github, we remove it from the provider and the database. -func (c *Controller) consolidateRunnerState() error { - scaleSetCli, err := scalesets.NewClient(c.ghCli) - if err != nil { - return fmt.Errorf("creating scaleset client: %w", err) - } - // Client is scoped to the current entity. Only runners in a repo/org/enterprise - // will be listed. - runners, err := scaleSetCli.ListAllRunners(c.ctx) - if err != nil { - return fmt.Errorf("listing runners: %w", err) - } - - byPoolID := make(map[string][]params.RunnerReference) - byScaleSetID := make(map[int][]params.RunnerReference) - for _, runner := range runners.RunnerReferences { - if runner.RunnerScaleSetID != 0 { - byScaleSetID[runner.RunnerScaleSetID] = append(byScaleSetID[runner.RunnerScaleSetID], runner) - } else { - poolID := poolIDFromLabels(runner) - if poolID == "" { - continue - } - byPoolID[poolID] = append(byPoolID[poolID], runner) - } - } - +// ConsolidateRunnerState will send a list of existing github runners to each scale set worker. +// The scale set worker will then need to cross check the existing runners in Github with the sate +// in the database. Any inconsistencies will b reconciliated. This cleans up any manually removed +// runners in either github or the providers. +func (c *Controller) ConsolidateRunnerState(byScaleSetID map[int][]params.RunnerReference) error { g, ctx := errgroup.WithContext(c.ctx) for _, scaleSet := range c.ScaleSets { runners := byScaleSetID[scaleSet.scaleSet.ScaleSetID] @@ -233,9 +207,6 @@ func (c *Controller) waitForErrorGroupOrContextCancelled(g *errgroup.Group) erro func (c *Controller) loop() { defer c.Stop() - consolidateTicker := time.NewTicker(common.PoolReapTimeoutInterval) - defer consolidateTicker.Stop() - for { select { case payload, ok := <-c.consumer.Watch(): @@ -247,17 +218,6 @@ func (c *Controller) loop() { c.handleWatcherEvent(payload) case <-c.ctx.Done(): return - case _, ok := <-consolidateTicker.C: - if !ok { - slog.InfoContext(c.ctx, "consolidate ticker closed") - return - } - if err := c.consolidateRunnerState(); err != nil { - if err := c.store.AddEntityEvent(c.ctx, c.Entity, params.StatusEvent, params.EventError, fmt.Sprintf("failed to consolidate runner state: %q", err.Error()), 30); err != nil { - slog.With(slog.Any("error", err)).Error("failed to add entity event") - } - slog.With(slog.Any("error", err)).Error("failed to consolidate runner state") - } case <-c.quit: return } diff --git a/workers/scaleset/controller_watcher.go b/workers/scaleset/controller_watcher.go index 6702e0f0a..8344cac57 100644 --- a/workers/scaleset/controller_watcher.go +++ b/workers/scaleset/controller_watcher.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package scaleset import ( @@ -6,8 +19,6 @@ import ( dbCommon "github.com/cloudbase/garm/database/common" "github.com/cloudbase/garm/params" - "github.com/cloudbase/garm/runner/common" - "github.com/cloudbase/garm/util/github" ) func (c *Controller) handleWatcherEvent(event dbCommon.ChangePayload) { @@ -19,9 +30,6 @@ func (c *Controller) handleWatcherEvent(event dbCommon.ChangePayload) { case entityType: slog.DebugContext(c.ctx, "got entity payload event") c.handleEntityEvent(event) - case dbCommon.GithubCredentialsEntityType: - slog.DebugContext(c.ctx, "got github credentials payload event") - c.handleCredentialsEvent(event) default: slog.ErrorContext(c.ctx, "invalid entity type", "entity_type", event.EntityType) return @@ -38,7 +46,7 @@ func (c *Controller) handleScaleSet(event dbCommon.ChangePayload) { switch event.Operation { case dbCommon.CreateOperation: slog.DebugContext(c.ctx, "got create operation for scale set", "scale_set_id", scaleSet.ID, "scale_set_name", scaleSet.Name) - if err := c.handleScaleSetCreateOperation(scaleSet, c.ghCli); err != nil { + if err := c.handleScaleSetCreateOperation(scaleSet); err != nil { slog.With(slog.Any("error", err)).ErrorContext(c.ctx, "failed to handle scale set create operation") } case dbCommon.UpdateOperation: @@ -57,7 +65,7 @@ func (c *Controller) handleScaleSet(event dbCommon.ChangePayload) { } } -func (c *Controller) handleScaleSetCreateOperation(sSet params.ScaleSet, ghCli common.GithubClient) error { +func (c *Controller) handleScaleSetCreateOperation(sSet params.ScaleSet) error { c.mux.Lock() defer c.mux.Unlock() @@ -74,7 +82,7 @@ func (c *Controller) handleScaleSetCreateOperation(sSet params.ScaleSet, ghCli c return fmt.Errorf("provider %s not found for scale set %s", sSet.ProviderName, sSet.Name) } - worker, err := NewWorker(c.ctx, c.store, sSet, provider, ghCli) + worker, err := NewWorker(c.ctx, c.store, sSet, provider) if err != nil { return fmt.Errorf("creating scale set worker: %w", err) } @@ -120,7 +128,7 @@ func (c *Controller) handleScaleSetUpdateOperation(sSet params.ScaleSet) error { // Some error may have occurred when the scale set was first created, so we // attempt to create it after the user updated the scale set, hopefully // fixing the reason for the failure. - return c.handleScaleSetCreateOperation(sSet, c.ghCli) + return c.handleScaleSetCreateOperation(sSet) } set.scaleSet = sSet c.ScaleSets[sSet.ID] = set @@ -128,44 +136,15 @@ func (c *Controller) handleScaleSetUpdateOperation(sSet params.ScaleSet) error { return nil } -func (c *Controller) handleCredentialsEvent(event dbCommon.ChangePayload) { - credentials, ok := event.Payload.(params.GithubCredentials) - if !ok { - slog.ErrorContext(c.ctx, "invalid credentials payload for entity type", "entity_type", event.EntityType, "payload", event) - return - } - - switch event.Operation { - case dbCommon.UpdateOperation: - slog.DebugContext(c.ctx, "got update operation") - c.mux.Lock() - defer c.mux.Unlock() - - if c.Entity.Credentials.ID != credentials.ID { - // stale update event. - return - } - c.Entity.Credentials = credentials - - if err := c.updateAndBroadcastCredentials(c.Entity); err != nil { - slog.With(slog.Any("error", err)).ErrorContext(c.ctx, "failed to update credentials") - return - } - default: - slog.ErrorContext(c.ctx, "invalid operation type", "operation_type", event.Operation) - return - } -} - func (c *Controller) handleEntityEvent(event dbCommon.ChangePayload) { var entityGetter params.EntityGetter var ok bool switch c.Entity.EntityType { - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: entityGetter, ok = event.Payload.(params.Repository) - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: entityGetter, ok = event.Payload.(params.Organization) - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: entityGetter, ok = event.Payload.(params.Enterprise) } if !ok { @@ -184,35 +163,9 @@ func (c *Controller) handleEntityEvent(event dbCommon.ChangePayload) { slog.DebugContext(c.ctx, "got update operation") c.mux.Lock() defer c.mux.Unlock() - - if c.Entity.Credentials.ID != entity.Credentials.ID { - // credentials were swapped on the entity. We need to recompose the watcher - // filters. - c.consumer.SetFilters(composeControllerWatcherFilters(entity)) - if err := c.updateAndBroadcastCredentials(c.Entity); err != nil { - slog.With(slog.Any("error", err)).ErrorContext(c.ctx, "failed to update credentials") - } - } c.Entity = entity default: slog.ErrorContext(c.ctx, "invalid operation type", "operation_type", event.Operation) return } } - -func (c *Controller) updateAndBroadcastCredentials(entity params.GithubEntity) error { - ghCli, err := github.Client(c.ctx, entity) - if err != nil { - return fmt.Errorf("creating github client: %w", err) - } - - c.ghCli = ghCli - - for _, scaleSet := range c.ScaleSets { - if err := scaleSet.worker.SetGithubClient(ghCli); err != nil { - slog.ErrorContext(c.ctx, "setting github client on worker", "error", err) - continue - } - } - return nil -} diff --git a/workers/scaleset/interfaces.go b/workers/scaleset/interfaces.go index ee089c255..b8acfceb9 100644 --- a/workers/scaleset/interfaces.go +++ b/workers/scaleset/interfaces.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package scaleset import ( @@ -6,8 +19,8 @@ import ( ) type scaleSetHelper interface { - ScaleSetCLI() *scalesets.ScaleSetClient GetScaleSet() params.ScaleSet + GetScaleSetClient() (*scalesets.ScaleSetClient, error) SetLastMessageID(id int64) error SetDesiredRunnerCount(count int) error Owner() string diff --git a/workers/scaleset/scaleset.go b/workers/scaleset/scaleset.go index 73d08c98a..e4faba701 100644 --- a/workers/scaleset/scaleset.go +++ b/workers/scaleset/scaleset.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package scaleset import ( @@ -17,19 +30,14 @@ import ( "github.com/cloudbase/garm/locking" "github.com/cloudbase/garm/params" "github.com/cloudbase/garm/runner/common" - "github.com/cloudbase/garm/util/github/scalesets" ) -func NewWorker(ctx context.Context, store dbCommon.Store, scaleSet params.ScaleSet, provider common.Provider, ghCli common.GithubClient) (*Worker, error) { +func NewWorker(ctx context.Context, store dbCommon.Store, scaleSet params.ScaleSet, provider common.Provider) (*Worker, error) { consumerID := fmt.Sprintf("scaleset-worker-%s-%d", scaleSet.Name, scaleSet.ID) controllerInfo, err := store.ControllerInfo() if err != nil { return nil, fmt.Errorf("getting controller info: %w", err) } - scaleSetCli, err := scalesets.NewClient(ghCli) - if err != nil { - return nil, fmt.Errorf("creating scale set client: %w", err) - } return &Worker{ ctx: ctx, controllerInfo: controllerInfo, @@ -37,8 +45,6 @@ func NewWorker(ctx context.Context, store dbCommon.Store, scaleSet params.ScaleS store: store, provider: provider, scaleSet: scaleSet, - ghCli: ghCli, - scaleSetCli: scaleSetCli, runners: make(map[string]params.Instance), }, nil } @@ -53,9 +59,7 @@ type Worker struct { scaleSet params.ScaleSet runners map[string]params.Instance - ghCli common.GithubClient - scaleSetCli *scalesets.ScaleSetClient - consumer dbCommon.Consumer + consumer dbCommon.Consumer listener *scaleSetListener @@ -110,7 +114,12 @@ func (w *Worker) Start() (err error) { instanceState := commonParams.InstancePendingDelete locking.Lock(instance.Name, w.consumerID) if instance.AgentID != 0 { - if err := w.scaleSetCli.RemoveRunner(w.ctx, instance.AgentID); err != nil { + scaleSetCli, err := w.GetScaleSetClient() + if err != nil { + slog.ErrorContext(w.ctx, "error getting scale set client", "error", err) + return fmt.Errorf("getting scale set client: %w", err) + } + if err := scaleSetCli.RemoveRunner(w.ctx, instance.AgentID); err != nil { // scale sets use JIT runners. This means that we create the runner in github // before we create the actual instance that will use the credentials. We need // to remove the runner from github if it exists. @@ -128,7 +137,7 @@ func (w *Worker) Start() (err error) { } // The runner may have come up, registered and is currently running a // job, in which case, github will not allow us to remove it. - runnerInstance, err := w.scaleSetCli.GetRunner(w.ctx, instance.AgentID) + runnerInstance, err := scaleSetCli.GetRunner(w.ctx, instance.AgentID) if err != nil { if !errors.Is(err, runnerErrors.ErrNotFound) { // We could not get info about the runner and it wasn't not found @@ -254,7 +263,11 @@ func (w *Worker) setRunnerDBStatus(runner string, status commonParams.InstanceSt } func (w *Worker) removeRunnerFromGithubAndSetPendingDelete(runnerName string, agentID int64) error { - if err := w.scaleSetCli.RemoveRunner(w.ctx, agentID); err != nil { + scaleSetCli, err := w.GetScaleSetClient() + if err != nil { + return fmt.Errorf("getting scale set client: %w", err) + } + if err := scaleSetCli.RemoveRunner(w.ctx, agentID); err != nil { if !errors.Is(err, runnerErrors.ErrNotFound) { return fmt.Errorf("removing runner %s: %w", runnerName, err) } @@ -321,6 +334,10 @@ func (w *Worker) consolidateRunnerState(runners []params.RunnerReference) error ghRunnersByName[runner.Name] = runner } + scaleSetCli, err := w.GetScaleSetClient() + if err != nil { + return fmt.Errorf("getting scale set client: %w", err) + } dbRunnersByName := w.runnerByName() // Cross check what exists in github with what we have in the database. for name, runner := range ghRunnersByName { @@ -329,7 +346,7 @@ func (w *Worker) consolidateRunnerState(runners []params.RunnerReference) error // runner appears to be active. Is it not managed by GARM? if status != params.RunnerIdle && status != params.RunnerActive { slog.InfoContext(w.ctx, "runner does not exist in GARM; removing from github", "runner_name", name) - if err := w.scaleSetCli.RemoveRunner(w.ctx, runner.ID); err != nil { + if err := scaleSetCli.RemoveRunner(w.ctx, runner.ID); err != nil { if errors.Is(err, runnerErrors.ErrNotFound) { continue } @@ -466,23 +483,6 @@ func (w *Worker) consolidateRunnerState(runners []params.RunnerReference) error return nil } -func (w *Worker) SetGithubClient(client common.GithubClient) error { - w.mux.Lock() - defer w.mux.Unlock() - - if err := w.listener.Stop(); err != nil { - slog.ErrorContext(w.ctx, "error stopping listener", "error", err) - } - - w.ghCli = client - scaleSetCli, err := scalesets.NewClient(client) - if err != nil { - return fmt.Errorf("error creating scale set client: %w", err) - } - w.scaleSetCli = scaleSetCli - return nil -} - func (w *Worker) pseudoPoolID() (string, error) { // This is temporary. We need to extend providers to know about scale sets. entity, err := w.scaleSet.GetEntity() @@ -563,8 +563,13 @@ func (w *Worker) handleInstanceEntityEvent(event dbCommon.ChangePayload) { w.mux.Unlock() return } + scaleSetCli, err := w.GetScaleSetClient() + if err != nil { + slog.ErrorContext(w.ctx, "error getting scale set client", "error", err) + return + } if oldInstance.RunnerStatus != instance.RunnerStatus && instance.RunnerStatus == params.RunnerIdle { - serviceRuner, err := w.scaleSetCli.GetRunner(w.ctx, instance.AgentID) + serviceRuner, err := scaleSetCli.GetRunner(w.ctx, instance.AgentID) if err != nil { slog.ErrorContext(w.ctx, "error getting runner details", "error", err) w.mux.Unlock() @@ -725,9 +730,14 @@ func (w *Worker) handleScaleUp(target, current uint) { return } + scaleSetCli, err := w.GetScaleSetClient() + if err != nil { + slog.ErrorContext(w.ctx, "error getting scale set client", "error", err) + return + } for i := current; i < target; i++ { newRunnerName := fmt.Sprintf("%s-%s", w.scaleSet.GetRunnerPrefix(), util.NewID()) - jitConfig, err := w.scaleSetCli.GenerateJitRunnerConfig(w.ctx, newRunnerName, w.scaleSet.ScaleSetID) + jitConfig, err := scaleSetCli.GenerateJitRunnerConfig(w.ctx, newRunnerName, w.scaleSet.ScaleSetID) if err != nil { slog.ErrorContext(w.ctx, "error generating jit config", "error", err) continue @@ -755,14 +765,14 @@ func (w *Worker) handleScaleUp(target, current uint) { dbInstance, err := w.store.CreateScaleSetInstance(w.ctx, w.scaleSet.ID, runnerParams) if err != nil { slog.ErrorContext(w.ctx, "error creating instance", "error", err) - if err := w.scaleSetCli.RemoveRunner(w.ctx, jitConfig.Runner.ID); err != nil { + if err := scaleSetCli.RemoveRunner(w.ctx, jitConfig.Runner.ID); err != nil { slog.ErrorContext(w.ctx, "error deleting runner", "error", err) } continue } w.runners[dbInstance.ID] = dbInstance - _, err = w.scaleSetCli.GetRunner(w.ctx, jitConfig.Runner.ID) + _, err = scaleSetCli.GetRunner(w.ctx, jitConfig.Runner.ID) if err != nil { slog.ErrorContext(w.ctx, "error getting runner details", "error", err) continue @@ -779,8 +789,11 @@ func (w *Worker) waitForToolsOrCancel() (hasTools, stopped bool) { if err != nil { slog.ErrorContext(w.ctx, "error getting entity", "error", err) } - _, ok := cache.GetGithubToolsCache(entity.ID) - return ok, false + if _, err := cache.GetGithubToolsCache(entity.ID); err != nil { + slog.DebugContext(w.ctx, "tools not found in cache; waiting for tools") + return false, false + } + return true, false case <-w.quit: return false, true case <-w.ctx.Done(): @@ -854,8 +867,13 @@ func (w *Worker) handleScaleDown(target, current uint) { continue } + scaleSetCli, err := w.GetScaleSetClient() + if err != nil { + slog.ErrorContext(w.ctx, "error getting scale set client", "error", err) + return + } slog.DebugContext(w.ctx, "removing runner", "runner_name", runner.Name) - if err := w.scaleSetCli.RemoveRunner(w.ctx, runner.AgentID); err != nil { + if err := scaleSetCli.RemoveRunner(w.ctx, runner.AgentID); err != nil { if !errors.Is(err, runnerErrors.ErrNotFound) { slog.ErrorContext(w.ctx, "error removing runner", "runner_name", runner.Name, "error", err) locking.Unlock(runner.Name, false) @@ -908,7 +926,7 @@ func (w *Worker) handleAutoScale() { } if !hasTools { - time.Sleep(1 * time.Second) + w.sleepWithCancel(1 * time.Second) continue } diff --git a/workers/scaleset/scaleset_helper.go b/workers/scaleset/scaleset_helper.go index c3302f752..7b3fdf030 100644 --- a/workers/scaleset/scaleset_helper.go +++ b/workers/scaleset/scaleset_helper.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package scaleset import ( @@ -7,13 +20,28 @@ import ( runnerErrors "github.com/cloudbase/garm-provider-common/errors" commonParams "github.com/cloudbase/garm-provider-common/params" + "github.com/cloudbase/garm/cache" "github.com/cloudbase/garm/locking" "github.com/cloudbase/garm/params" "github.com/cloudbase/garm/util/github/scalesets" ) -func (w *Worker) ScaleSetCLI() *scalesets.ScaleSetClient { - return w.scaleSetCli +func (w *Worker) GetScaleSetClient() (*scalesets.ScaleSetClient, error) { + scaleSetEntity, err := w.scaleSet.GetEntity() + if err != nil { + return nil, fmt.Errorf("getting entity: %w", err) + } + + ghCli, ok := cache.GetGithubClient(scaleSetEntity.ID) + if !ok { + return nil, fmt.Errorf("getting github client: %w", err) + } + scaleSetClient, err := scalesets.NewClient(ghCli) + if err != nil { + return nil, fmt.Errorf("creating scale set client: %w", err) + } + + return scaleSetClient, nil } func (w *Worker) GetScaleSet() params.ScaleSet { @@ -45,11 +73,11 @@ func (w *Worker) recordOrUpdateJob(job params.ScaleSetJobMessage) error { jobParams.RunnerGroupName = w.scaleSet.GitHubRunnerGroup switch entity.EntityType { - case params.GithubEntityTypeEnterprise: + case params.ForgeEntityTypeEnterprise: jobParams.EnterpriseID = &asUUID - case params.GithubEntityTypeRepository: + case params.ForgeEntityTypeRepository: jobParams.RepoID = &asUUID - case params.GithubEntityTypeOrganization: + case params.ForgeEntityTypeOrganization: jobParams.OrgID = &asUUID default: return fmt.Errorf("unknown entity type: %s", entity.EntityType) diff --git a/workers/scaleset/scaleset_listener.go b/workers/scaleset/scaleset_listener.go index 7a521e464..76a321f4a 100644 --- a/workers/scaleset/scaleset_listener.go +++ b/workers/scaleset/scaleset_listener.go @@ -1,3 +1,16 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package scaleset import ( @@ -48,8 +61,12 @@ func (l *scaleSetListener) Start() error { l.listenerCtx, l.cancelFunc = context.WithCancel(context.Background()) scaleSet := l.scaleSetHelper.GetScaleSet() + scaleSetClient, err := l.scaleSetHelper.GetScaleSetClient() + if err != nil { + return fmt.Errorf("getting scale set client: %w", err) + } slog.DebugContext(l.ctx, "creating new message session", "scale_set", scaleSet.ScaleSetID) - session, err := l.scaleSetHelper.ScaleSetCLI().CreateMessageSession( + session, err := scaleSetClient.CreateMessageSession( l.listenerCtx, scaleSet.ScaleSetID, l.scaleSetHelper.Owner(), ) @@ -72,13 +89,16 @@ func (l *scaleSetListener) Stop() error { if !l.running { return nil } - + scaleSetClient, err := l.scaleSetHelper.GetScaleSetClient() + if err != nil { + return fmt.Errorf("getting scale set client: %w", err) + } if l.messageSession != nil { slog.DebugContext(l.ctx, "closing message session", "scale_set", l.scaleSetHelper.GetScaleSet().ScaleSetID) if err := l.messageSession.Close(); err != nil { slog.ErrorContext(l.ctx, "closing message session", "error", err) } - if err := l.scaleSetHelper.ScaleSetCLI().DeleteMessageSession(context.Background(), l.messageSession); err != nil { + if err := scaleSetClient.DeleteMessageSession(context.Background(), l.messageSession); err != nil { slog.ErrorContext(l.ctx, "error deleting message session", "error", err) } } @@ -145,12 +165,17 @@ func (l *scaleSetListener) handleSessionMessage(msg params.RunnerScaleSetMessage } } + scaleSetClient, err := l.scaleSetHelper.GetScaleSetClient() + if err != nil { + slog.ErrorContext(l.ctx, "getting scale set client", "error", err) + return + } if len(availableJobs) > 0 { jobIDs := make([]int64, len(availableJobs)) for idx, job := range availableJobs { jobIDs[idx] = job.RunnerRequestID } - idsAcquired, err := l.scaleSetHelper.ScaleSetCLI().AcquireJobs( + idsAcquired, err := scaleSetClient.AcquireJobs( l.listenerCtx, l.scaleSetHelper.GetScaleSet().ScaleSetID, l.messageSession.MessageQueueAccessToken(), jobIDs) if err != nil { diff --git a/workers/scaleset/util.go b/workers/scaleset/util.go index aa3156c76..7852cb89a 100644 --- a/workers/scaleset/util.go +++ b/workers/scaleset/util.go @@ -1,14 +1,25 @@ +// Copyright 2025 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. package scaleset import ( - "strings" - dbCommon "github.com/cloudbase/garm/database/common" "github.com/cloudbase/garm/database/watcher" "github.com/cloudbase/garm/params" ) -func composeControllerWatcherFilters(entity params.GithubEntity) dbCommon.PayloadFilterFunc { +func composeControllerWatcherFilters(entity params.ForgeEntity) dbCommon.PayloadFilterFunc { return watcher.WithAny( watcher.WithAll( watcher.WithEntityScaleSetFilter(entity), @@ -22,18 +33,5 @@ func composeControllerWatcherFilters(entity params.GithubEntity) dbCommon.Payloa watcher.WithEntityFilter(entity), watcher.WithOperationTypeFilter(dbCommon.UpdateOperation), ), - watcher.WithAll( - watcher.WithGithubCredentialsFilter(entity.Credentials), - watcher.WithOperationTypeFilter(dbCommon.UpdateOperation), - ), ) } - -func poolIDFromLabels(runner params.RunnerReference) string { - for _, lbl := range runner.Labels { - if strings.HasPrefix(lbl.Name, poolIDLabelprefix) { - return lbl.Name[len(poolIDLabelprefix):] - } - } - return "" -}