Move URLs from default section of config to DB

This change moves the callback_url, metadata_url and webhooks_url from
the config to the database. The goal is to move as much as possible from
the config to the DB, in preparation for a potential refactor that will
allow GARM to scale out. This would allow multiple nodes to share a single
source of truth.

Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
This commit is contained in:
Gabriel Adrian Samfira 2024-06-05 06:41:16 +00:00
parent 7ee235aeb0
commit 9748aa47af
22 changed files with 1067 additions and 177 deletions

View file

@ -391,3 +391,42 @@ func (a *APIController) ControllerInfoHandler(w http.ResponseWriter, r *http.Req
slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response") slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response")
} }
} }
// swagger:route PUT /controller controller UpdateController
//
// Update controller.
//
// Parameters:
// + name: Body
// description: Parameters used when updating the controller.
// type: UpdateControllerParams
// in: body
// required: true
//
// Responses:
// 200: ControllerInfo
// 400: APIErrorResponse
func (a *APIController) UpdateControllerHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var updateParams runnerParams.UpdateControllerParams
if err := json.NewDecoder(r.Body).Decode(&updateParams); err != nil {
handleError(ctx, w, gErrors.ErrBadRequest)
return
}
if err := updateParams.Validate(); err != nil {
handleError(ctx, w, err)
return
}
info, err := a.r.UpdateController(ctx, updateParams)
if err != nil {
handleError(ctx, w, err)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(info); err != nil {
slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response")
}
}

View file

@ -36,4 +36,9 @@ var (
Error: "init_required", Error: "init_required",
Details: "Missing superuser", Details: "Missing superuser",
} }
// URLsRequired is returned if the controller does not have the required URLs
URLsRequired = APIErrorResponse{
Error: "urls_required",
Details: "Missing required URLs. Make sure you update the metadata, callback and webhook URLs",
}
) )

View file

@ -100,7 +100,7 @@ func requestLogger(h http.Handler) http.Handler {
}) })
} }
func NewAPIRouter(han *controllers.APIController, authMiddleware, initMiddleware, instanceMiddleware auth.Middleware, manageWebhooks bool) *mux.Router { func NewAPIRouter(han *controllers.APIController, authMiddleware, initMiddleware, urlsRequiredMiddleware, instanceMiddleware auth.Middleware, manageWebhooks bool) *mux.Router {
router := mux.NewRouter() router := mux.NewRouter()
router.Use(requestLogger) router.Use(requestLogger)
@ -152,11 +152,38 @@ func NewAPIRouter(han *controllers.APIController, authMiddleware, initMiddleware
authRouter.Handle("/{login:login\\/?}", http.HandlerFunc(han.LoginHandler)).Methods("POST", "OPTIONS") authRouter.Handle("/{login:login\\/?}", http.HandlerFunc(han.LoginHandler)).Methods("POST", "OPTIONS")
authRouter.Use(initMiddleware.Middleware) authRouter.Use(initMiddleware.Middleware)
//////////////////////////
// Controller endpoints //
//////////////////////////
controllerRouter := apiSubRouter.PathPrefix("/controller").Subrouter()
// The controller endpoints allow us to get information about the controller and update the URL endpoints.
// This endpoint must not be guarded by the urlsRequiredMiddleware as that would prevent the user from
// updating the URLs.
controllerRouter.Use(initMiddleware.Middleware)
controllerRouter.Use(authMiddleware.Middleware)
controllerRouter.Use(auth.AdminRequiredMiddleware)
// Get controller info
controllerRouter.Handle("/", http.HandlerFunc(han.ControllerInfoHandler)).Methods("GET", "OPTIONS")
controllerRouter.Handle("", http.HandlerFunc(han.ControllerInfoHandler)).Methods("GET", "OPTIONS")
// Update controller
controllerRouter.Handle("/", http.HandlerFunc(han.UpdateControllerHandler)).Methods("PUT", "OPTIONS")
controllerRouter.Handle("", http.HandlerFunc(han.UpdateControllerHandler)).Methods("PUT", "OPTIONS")
////////////////////////////////////
// API router for everything else //
////////////////////////////////////
apiRouter := apiSubRouter.PathPrefix("").Subrouter() apiRouter := apiSubRouter.PathPrefix("").Subrouter()
apiRouter.Use(initMiddleware.Middleware) apiRouter.Use(initMiddleware.Middleware)
// all endpoints except the controller endpoint should return an error
// if the required metadata, callback and webhook URLs are not set.
apiRouter.Use(urlsRequiredMiddleware.Middleware)
apiRouter.Use(authMiddleware.Middleware) apiRouter.Use(authMiddleware.Middleware)
apiRouter.Use(auth.AdminRequiredMiddleware) apiRouter.Use(auth.AdminRequiredMiddleware)
// Legacy controller path
apiRouter.Handle("/controller-info/", http.HandlerFunc(han.ControllerInfoHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/controller-info", http.HandlerFunc(han.ControllerInfoHandler)).Methods("GET", "OPTIONS")
// Metrics Token // Metrics Token
apiRouter.Handle("/metrics-token/", http.HandlerFunc(han.MetricsTokenHandler)).Methods("GET", "OPTIONS") apiRouter.Handle("/metrics-token/", http.HandlerFunc(han.MetricsTokenHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/metrics-token", http.HandlerFunc(han.MetricsTokenHandler)).Methods("GET", "OPTIONS") apiRouter.Handle("/metrics-token", http.HandlerFunc(han.MetricsTokenHandler)).Methods("GET", "OPTIONS")
@ -343,10 +370,6 @@ func NewAPIRouter(han *controllers.APIController, authMiddleware, initMiddleware
apiRouter.Handle("/providers/", http.HandlerFunc(han.ListProviders)).Methods("GET", "OPTIONS") apiRouter.Handle("/providers/", http.HandlerFunc(han.ListProviders)).Methods("GET", "OPTIONS")
apiRouter.Handle("/providers", http.HandlerFunc(han.ListProviders)).Methods("GET", "OPTIONS") apiRouter.Handle("/providers", http.HandlerFunc(han.ListProviders)).Methods("GET", "OPTIONS")
// Controller info
apiRouter.Handle("/controller-info/", http.HandlerFunc(han.ControllerInfoHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/controller-info", http.HandlerFunc(han.ControllerInfoHandler)).Methods("GET", "OPTIONS")
////////////////////// //////////////////////
// Github Endpoints // // Github Endpoints //
////////////////////// //////////////////////

View file

@ -278,3 +278,10 @@ definitions:
import: import:
package: github.com/cloudbase/garm/params package: github.com/cloudbase/garm/params
alias: garm_params alias: garm_params
UpdateControllerParams:
type: object
x-go-type:
type: UpdateControllerParams
import:
package: github.com/cloudbase/garm/params
alias: garm_params

View file

@ -244,6 +244,13 @@ definitions:
alias: garm_params alias: garm_params
package: github.com/cloudbase/garm/params package: github.com/cloudbase/garm/params
type: Repository type: Repository
UpdateControllerParams:
type: object
x-go-type:
import:
alias: garm_params
package: github.com/cloudbase/garm/params
type: UpdateControllerParams
UpdateEntityParams: UpdateEntityParams:
type: object type: object
x-go-type: x-go-type:
@ -311,6 +318,30 @@ paths:
summary: Logs in a user and returns a JWT token. summary: Logs in a user and returns a JWT token.
tags: tags:
- login - login
/controller:
put:
operationId: UpdateController
parameters:
- description: Parameters used when updating the controller.
in: body
name: Body
required: true
schema:
$ref: '#/definitions/UpdateControllerParams'
description: Parameters used when updating the controller.
type: object
responses:
"200":
description: ControllerInfo
schema:
$ref: '#/definitions/ControllerInfo'
"400":
description: APIErrorResponse
schema:
$ref: '#/definitions/APIErrorResponse'
summary: Update controller.
tags:
- controller
/controller-info: /controller-info:
get: get:
operationId: ControllerInfo operationId: ControllerInfo

View file

@ -51,3 +51,30 @@ func (i *initRequired) Middleware(next http.Handler) http.Handler {
next.ServeHTTP(w, r.WithContext(ctx)) next.ServeHTTP(w, r.WithContext(ctx))
}) })
} }
func NewUrlsRequiredMiddleware(store common.Store) (Middleware, error) {
return &urlsRequired{
store: store,
}, nil
}
type urlsRequired struct {
store common.Store
}
func (u *urlsRequired) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctrlInfo, err := u.store.ControllerInfo()
if err != nil || ctrlInfo.WebhookURL == "" || ctrlInfo.MetadataURL == "" || ctrlInfo.CallbackURL == "" {
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusConflict)
if err := json.NewEncoder(w).Encode(params.URLsRequired); err != nil {
slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response")
}
return
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View file

@ -0,0 +1,106 @@
// Code generated by go-swagger; DO NOT EDIT.
package controller
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"fmt"
"github.com/go-openapi/runtime"
httptransport "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
)
// New creates a new controller API client.
func New(transport runtime.ClientTransport, formats strfmt.Registry) ClientService {
return &Client{transport: transport, formats: formats}
}
// New creates a new controller API client with basic auth credentials.
// It takes the following parameters:
// - host: http host (github.com).
// - basePath: any base path for the API client ("/v1", "/v3").
// - scheme: http scheme ("http", "https").
// - user: user for basic authentication header.
// - password: password for basic authentication header.
func NewClientWithBasicAuth(host, basePath, scheme, user, password string) ClientService {
transport := httptransport.New(host, basePath, []string{scheme})
transport.DefaultAuthentication = httptransport.BasicAuth(user, password)
return &Client{transport: transport, formats: strfmt.Default}
}
// New creates a new controller API client with a bearer token for authentication.
// It takes the following parameters:
// - host: http host (github.com).
// - basePath: any base path for the API client ("/v1", "/v3").
// - scheme: http scheme ("http", "https").
// - bearerToken: bearer token for Bearer authentication header.
func NewClientWithBearerToken(host, basePath, scheme, bearerToken string) ClientService {
transport := httptransport.New(host, basePath, []string{scheme})
transport.DefaultAuthentication = httptransport.BearerToken(bearerToken)
return &Client{transport: transport, formats: strfmt.Default}
}
/*
Client for controller API
*/
type Client struct {
transport runtime.ClientTransport
formats strfmt.Registry
}
// ClientOption may be used to customize the behavior of Client methods.
type ClientOption func(*runtime.ClientOperation)
// ClientService is the interface for Client methods
type ClientService interface {
UpdateController(params *UpdateControllerParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UpdateControllerOK, error)
SetTransport(transport runtime.ClientTransport)
}
/*
UpdateController updates controller
*/
func (a *Client) UpdateController(params *UpdateControllerParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UpdateControllerOK, error) {
// TODO: Validate the params before sending
if params == nil {
params = NewUpdateControllerParams()
}
op := &runtime.ClientOperation{
ID: "UpdateController",
Method: "PUT",
PathPattern: "/controller",
ProducesMediaTypes: []string{"application/json"},
ConsumesMediaTypes: []string{"application/json"},
Schemes: []string{"http"},
Params: params,
Reader: &UpdateControllerReader{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.(*UpdateControllerOK)
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 UpdateController: 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
}

View file

@ -0,0 +1,151 @@
// Code generated by go-swagger; DO NOT EDIT.
package controller
// 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"
)
// NewUpdateControllerParams creates a new UpdateControllerParams 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 NewUpdateControllerParams() *UpdateControllerParams {
return &UpdateControllerParams{
timeout: cr.DefaultTimeout,
}
}
// NewUpdateControllerParamsWithTimeout creates a new UpdateControllerParams object
// with the ability to set a timeout on a request.
func NewUpdateControllerParamsWithTimeout(timeout time.Duration) *UpdateControllerParams {
return &UpdateControllerParams{
timeout: timeout,
}
}
// NewUpdateControllerParamsWithContext creates a new UpdateControllerParams object
// with the ability to set a context for a request.
func NewUpdateControllerParamsWithContext(ctx context.Context) *UpdateControllerParams {
return &UpdateControllerParams{
Context: ctx,
}
}
// NewUpdateControllerParamsWithHTTPClient creates a new UpdateControllerParams object
// with the ability to set a custom HTTPClient for a request.
func NewUpdateControllerParamsWithHTTPClient(client *http.Client) *UpdateControllerParams {
return &UpdateControllerParams{
HTTPClient: client,
}
}
/*
UpdateControllerParams contains all the parameters to send to the API endpoint
for the update controller operation.
Typically these are written to a http.Request.
*/
type UpdateControllerParams struct {
/* Body.
Parameters used when updating the controller.
*/
Body garm_params.UpdateControllerParams
timeout time.Duration
Context context.Context
HTTPClient *http.Client
}
// WithDefaults hydrates default values in the update controller params (not the query body).
//
// All values with no default are reset to their zero value.
func (o *UpdateControllerParams) WithDefaults() *UpdateControllerParams {
o.SetDefaults()
return o
}
// SetDefaults hydrates default values in the update controller params (not the query body).
//
// All values with no default are reset to their zero value.
func (o *UpdateControllerParams) SetDefaults() {
// no default values defined for this parameter
}
// WithTimeout adds the timeout to the update controller params
func (o *UpdateControllerParams) WithTimeout(timeout time.Duration) *UpdateControllerParams {
o.SetTimeout(timeout)
return o
}
// SetTimeout adds the timeout to the update controller params
func (o *UpdateControllerParams) SetTimeout(timeout time.Duration) {
o.timeout = timeout
}
// WithContext adds the context to the update controller params
func (o *UpdateControllerParams) WithContext(ctx context.Context) *UpdateControllerParams {
o.SetContext(ctx)
return o
}
// SetContext adds the context to the update controller params
func (o *UpdateControllerParams) SetContext(ctx context.Context) {
o.Context = ctx
}
// WithHTTPClient adds the HTTPClient to the update controller params
func (o *UpdateControllerParams) WithHTTPClient(client *http.Client) *UpdateControllerParams {
o.SetHTTPClient(client)
return o
}
// SetHTTPClient adds the HTTPClient to the update controller params
func (o *UpdateControllerParams) SetHTTPClient(client *http.Client) {
o.HTTPClient = client
}
// WithBody adds the body to the update controller params
func (o *UpdateControllerParams) WithBody(body garm_params.UpdateControllerParams) *UpdateControllerParams {
o.SetBody(body)
return o
}
// SetBody adds the body to the update controller params
func (o *UpdateControllerParams) SetBody(body garm_params.UpdateControllerParams) {
o.Body = body
}
// WriteToRequest writes these params to a swagger request
func (o *UpdateControllerParams) 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
}

View file

@ -0,0 +1,179 @@
// Code generated by go-swagger; DO NOT EDIT.
package controller
// 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"
)
// UpdateControllerReader is a Reader for the UpdateController structure.
type UpdateControllerReader struct {
formats strfmt.Registry
}
// ReadResponse reads a server response into the received o.
func (o *UpdateControllerReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
switch response.Code() {
case 200:
result := NewUpdateControllerOK()
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
return result, nil
case 400:
result := NewUpdateControllerBadRequest()
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
return nil, result
default:
return nil, runtime.NewAPIError("[PUT /controller] UpdateController", response, response.Code())
}
}
// NewUpdateControllerOK creates a UpdateControllerOK with default headers values
func NewUpdateControllerOK() *UpdateControllerOK {
return &UpdateControllerOK{}
}
/*
UpdateControllerOK describes a response with status code 200, with default header values.
ControllerInfo
*/
type UpdateControllerOK struct {
Payload garm_params.ControllerInfo
}
// IsSuccess returns true when this update controller o k response has a 2xx status code
func (o *UpdateControllerOK) IsSuccess() bool {
return true
}
// IsRedirect returns true when this update controller o k response has a 3xx status code
func (o *UpdateControllerOK) IsRedirect() bool {
return false
}
// IsClientError returns true when this update controller o k response has a 4xx status code
func (o *UpdateControllerOK) IsClientError() bool {
return false
}
// IsServerError returns true when this update controller o k response has a 5xx status code
func (o *UpdateControllerOK) IsServerError() bool {
return false
}
// IsCode returns true when this update controller o k response a status code equal to that given
func (o *UpdateControllerOK) IsCode(code int) bool {
return code == 200
}
// Code gets the status code for the update controller o k response
func (o *UpdateControllerOK) Code() int {
return 200
}
func (o *UpdateControllerOK) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[PUT /controller][%d] updateControllerOK %s", 200, payload)
}
func (o *UpdateControllerOK) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[PUT /controller][%d] updateControllerOK %s", 200, payload)
}
func (o *UpdateControllerOK) GetPayload() garm_params.ControllerInfo {
return o.Payload
}
func (o *UpdateControllerOK) 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
}
// NewUpdateControllerBadRequest creates a UpdateControllerBadRequest with default headers values
func NewUpdateControllerBadRequest() *UpdateControllerBadRequest {
return &UpdateControllerBadRequest{}
}
/*
UpdateControllerBadRequest describes a response with status code 400, with default header values.
APIErrorResponse
*/
type UpdateControllerBadRequest struct {
Payload apiserver_params.APIErrorResponse
}
// IsSuccess returns true when this update controller bad request response has a 2xx status code
func (o *UpdateControllerBadRequest) IsSuccess() bool {
return false
}
// IsRedirect returns true when this update controller bad request response has a 3xx status code
func (o *UpdateControllerBadRequest) IsRedirect() bool {
return false
}
// IsClientError returns true when this update controller bad request response has a 4xx status code
func (o *UpdateControllerBadRequest) IsClientError() bool {
return true
}
// IsServerError returns true when this update controller bad request response has a 5xx status code
func (o *UpdateControllerBadRequest) IsServerError() bool {
return false
}
// IsCode returns true when this update controller bad request response a status code equal to that given
func (o *UpdateControllerBadRequest) IsCode(code int) bool {
return code == 400
}
// Code gets the status code for the update controller bad request response
func (o *UpdateControllerBadRequest) Code() int {
return 400
}
func (o *UpdateControllerBadRequest) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[PUT /controller][%d] updateControllerBadRequest %s", 400, payload)
}
func (o *UpdateControllerBadRequest) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[PUT /controller][%d] updateControllerBadRequest %s", 400, payload)
}
func (o *UpdateControllerBadRequest) GetPayload() apiserver_params.APIErrorResponse {
return o.Payload
}
func (o *UpdateControllerBadRequest) 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
}

View file

@ -10,6 +10,7 @@ import (
httptransport "github.com/go-openapi/runtime/client" httptransport "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
"github.com/cloudbase/garm/client/controller"
"github.com/cloudbase/garm/client/controller_info" "github.com/cloudbase/garm/client/controller_info"
"github.com/cloudbase/garm/client/credentials" "github.com/cloudbase/garm/client/credentials"
"github.com/cloudbase/garm/client/endpoints" "github.com/cloudbase/garm/client/endpoints"
@ -67,6 +68,7 @@ func New(transport runtime.ClientTransport, formats strfmt.Registry) *GarmAPI {
cli := new(GarmAPI) cli := new(GarmAPI)
cli.Transport = transport cli.Transport = transport
cli.Controller = controller.New(transport, formats)
cli.ControllerInfo = controller_info.New(transport, formats) cli.ControllerInfo = controller_info.New(transport, formats)
cli.Credentials = credentials.New(transport, formats) cli.Credentials = credentials.New(transport, formats)
cli.Endpoints = endpoints.New(transport, formats) cli.Endpoints = endpoints.New(transport, formats)
@ -124,6 +126,8 @@ func (cfg *TransportConfig) WithSchemes(schemes []string) *TransportConfig {
// GarmAPI is a client for garm API // GarmAPI is a client for garm API
type GarmAPI struct { type GarmAPI struct {
Controller controller.ClientService
ControllerInfo controller_info.ClientService ControllerInfo controller_info.ClientService
Credentials credentials.ClientService Credentials credentials.ClientService
@ -156,6 +160,7 @@ type GarmAPI struct {
// SetTransport changes the transport on the client and all its subresources // SetTransport changes the transport on the client and all its subresources
func (c *GarmAPI) SetTransport(transport runtime.ClientTransport) { func (c *GarmAPI) SetTransport(transport runtime.ClientTransport) {
c.Transport = transport c.Transport = transport
c.Controller.SetTransport(transport)
c.ControllerInfo.SetTransport(transport) c.ControllerInfo.SetTransport(transport)
c.Credentials.SetTransport(transport) c.Credentials.SetTransport(transport)
c.Endpoints.SetTransport(transport) c.Endpoints.SetTransport(transport)

View file

@ -0,0 +1,173 @@
// Copyright 2023 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/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
apiClientController "github.com/cloudbase/garm/client/controller"
apiClientControllerInfo "github.com/cloudbase/garm/client/controller_info"
"github.com/cloudbase/garm/params"
)
var controllerCmd = &cobra.Command{
Use: "controller",
Aliases: []string{"controller-info"},
SilenceUsage: true,
Short: "Controller operations",
Long: `Query or update information about the current controller.`,
Run: nil,
}
var controllerShowCmd = &cobra.Command{
Use: "show",
Short: "Show information",
Long: `Show information about the current controller.`,
SilenceUsage: true,
RunE: func(_ *cobra.Command, _ []string) error {
if needsInit {
return errNeedsInitError
}
showInfo := apiClientControllerInfo.NewControllerInfoParams()
response, err := apiCli.ControllerInfo.ControllerInfo(showInfo, authToken)
if err != nil {
return err
}
return formatInfo(response.Payload)
},
}
var controllerUpdateCmd = &cobra.Command{
Use: "update",
Short: "Update controller information",
Long: `Update information about the current controller.
Warning: Dragons ahead, please read carefully.
Changing the URLs for the controller metadata, callback and webhooks, will
impact the controller's ability to manage webhooks and runners.
As GARM can be set up behind a reverse proxy or through several layers of
network address translation or load balancing, we need to explicitly tell
GARM how to reach each of these URLs. Internally, GARM sets up API endpoints
as follows:
* /webhooks - the base URL for the webhooks. Github needs to reach this URL.
* /api/v1/metadata - the metadata URL. Your runners need to be able to reach this URL.
* /api/v1/callbacks - the callback URL. Your runners need to be able to reach this URL.
You need to expose these endpoints to the interested parties (github or
your runners), then you need to update the controller with the URLs you set up.
For example, if you set the webhooks URL in your reverse proxy to
https://garm.example.com/garm-hooks, this still needs to point to the "/webhooks"
URL in the GARM backend, but in the controller info you need to set the URL to
https://garm.example.com/garm-hooks using:
garm-cli controller update --webhook-url=https://garm.example.com/garm-hooks
If you expose GARM to the outside world directly, or if you don't rewrite the URLs
above in your reverse proxy config, use the above 3 endpoints without change,
substituting garm.example.com with the correct hostname or IP address.
In most cases, you will have a GARM backend (say 192.168.100.10) and a reverse
proxy in front of it exposed as https://garm.example.com. If you don't rewrite
the URLs in the reverse proxy, and you just point to your backend, you can set
up the GARM controller URLs as:
garm-cli controller update \
--webhook-url=https://garm.example.com/webhooks \
--metadata-url=https://garm.example.com/api/v1/metadata \
--callback-url=https://garm.example.com/api/v1/callbacks
`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, _ []string) error {
if needsInit {
return errNeedsInitError
}
params := params.UpdateControllerParams{}
if cmd.Flags().Changed("metadata-url") {
params.MetadataURL = &metadataURL
}
if cmd.Flags().Changed("callback-url") {
params.CallbackURL = &callbackURL
}
if cmd.Flags().Changed("webhook-url") {
params.WebhookURL = &webhookURL
}
if params.WebhookURL == nil && params.MetadataURL == nil && params.CallbackURL == nil {
cmd.Help()
return fmt.Errorf("at least one of metadata-url, callback-url or webhook-url must be provided")
}
updateUrlsReq := apiClientController.NewUpdateControllerParams()
updateUrlsReq.Body = params
info, err := apiCli.Controller.UpdateController(updateUrlsReq, authToken)
if err != nil {
return fmt.Errorf("error updating controller: %w", err)
}
formatInfo(info.Payload)
return nil
},
}
func renderControllerInfoTable(info params.ControllerInfo) string {
t := table.NewWriter()
header := table.Row{"Field", "Value"}
if info.WebhookURL == "" {
info.WebhookURL = "N/A"
}
if info.ControllerWebhookURL == "" {
info.ControllerWebhookURL = "N/A"
}
t.AppendHeader(header)
t.AppendRow(table.Row{"Controller ID", info.ControllerID})
if info.Hostname != "" {
t.AppendRow(table.Row{"Hostname", info.Hostname})
}
t.AppendRow(table.Row{"Metadata URL", info.MetadataURL})
t.AppendRow(table.Row{"Callback URL", info.CallbackURL})
t.AppendRow(table.Row{"Webhook Base URL", info.WebhookURL})
t.AppendRow(table.Row{"Controller Webhook URL", info.ControllerWebhookURL})
return t.Render()
}
func formatInfo(info params.ControllerInfo) error {
fmt.Println(renderControllerInfoTable(info))
return nil
}
func init() {
controllerUpdateCmd.Flags().StringVarP(&metadataURL, "metadata-url", "m", "", "The metadata URL for the controller (ie. https://garm.example.com/api/v1/metadata)")
controllerUpdateCmd.Flags().StringVarP(&callbackURL, "callback-url", "c", "", "The callback URL for the controller (ie. https://garm.example.com/api/v1/callbacks)")
controllerUpdateCmd.Flags().StringVarP(&webhookURL, "webhook-url", "w", "", "The webhook URL for the controller (ie. https://garm.example.com/webhooks)")
controllerCmd.AddCommand(
controllerShowCmd,
controllerUpdateCmd,
)
rootCmd.AddCommand(controllerCmd)
}

View file

@ -1,84 +0,0 @@
// Copyright 2023 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/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
apiClientControllerInfo "github.com/cloudbase/garm/client/controller_info"
"github.com/cloudbase/garm/params"
)
var infoCmd = &cobra.Command{
Use: "controller-info",
SilenceUsage: true,
Short: "Information about controller",
Long: `Query information about the current controller.`,
Run: nil,
}
var infoShowCmd = &cobra.Command{
Use: "show",
Short: "Show information",
Long: `Show information about the current controller.`,
SilenceUsage: true,
RunE: func(_ *cobra.Command, _ []string) error {
if needsInit {
return errNeedsInitError
}
showInfo := apiClientControllerInfo.NewControllerInfoParams()
response, err := apiCli.ControllerInfo.ControllerInfo(showInfo, authToken)
if err != nil {
return err
}
return formatInfo(response.Payload)
},
}
func formatInfo(info params.ControllerInfo) error {
t := table.NewWriter()
header := table.Row{"Field", "Value"}
if info.WebhookURL == "" {
info.WebhookURL = "N/A"
}
if info.ControllerWebhookURL == "" {
info.ControllerWebhookURL = "N/A"
}
t.AppendHeader(header)
t.AppendRow(table.Row{"Controller ID", info.ControllerID})
t.AppendRow(table.Row{"Hostname", info.Hostname})
t.AppendRow(table.Row{"Metadata URL", info.MetadataURL})
t.AppendRow(table.Row{"Callback URL", info.CallbackURL})
t.AppendRow(table.Row{"Webhook Base URL", info.WebhookURL})
t.AppendRow(table.Row{"Controller Webhook URL", info.ControllerWebhookURL})
fmt.Println(t.Render())
return nil
}
func init() {
infoCmd.AddCommand(
infoShowCmd,
)
rootCmd.AddCommand(infoCmd)
}

View file

@ -16,12 +16,15 @@ package cmd
import ( import (
"fmt" "fmt"
"net/url"
"strings" "strings"
openapiRuntimeClient "github.com/go-openapi/runtime/client"
"github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/table"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
apiClientController "github.com/cloudbase/garm/client/controller"
apiClientFirstRun "github.com/cloudbase/garm/client/first_run" apiClientFirstRun "github.com/cloudbase/garm/client/first_run"
apiClientLogin "github.com/cloudbase/garm/client/login" apiClientLogin "github.com/cloudbase/garm/client/login"
"github.com/cloudbase/garm/cmd/garm-cli/common" "github.com/cloudbase/garm/cmd/garm-cli/common"
@ -29,6 +32,12 @@ import (
"github.com/cloudbase/garm/params" "github.com/cloudbase/garm/params"
) )
var (
callbackURL string
metadataURL string
webhookURL string
)
// initCmd represents the init command // initCmd represents the init command
var initCmd = &cobra.Command{ var initCmd = &cobra.Command{
Use: "init", Use: "init",
@ -52,10 +61,13 @@ garm-cli init --name=dev --url=https://runner.example.com --username=admin --pas
} }
} }
url := strings.TrimSuffix(loginURL, "/")
if err := promptUnsetInitVariables(); err != nil { if err := promptUnsetInitVariables(); err != nil {
return err return err
} }
ensureDefaultEndpoints(url)
newUserReq := apiClientFirstRun.NewFirstRunParams() newUserReq := apiClientFirstRun.NewFirstRunParams()
newUserReq.Body = params.NewUserParams{ newUserReq.Body = params.NewUserParams{
Username: loginUserName, Username: loginUserName,
@ -63,9 +75,6 @@ garm-cli init --name=dev --url=https://runner.example.com --username=admin --pas
FullName: loginFullName, FullName: loginFullName,
Email: loginEmail, Email: loginEmail,
} }
url := strings.TrimSuffix(loginURL, "/")
initAPIClient(url, "") initAPIClient(url, "")
response, err := apiCli.FirstRun.FirstRun(newUserReq, authToken) response, err := apiCli.FirstRun.FirstRun(newUserReq, authToken)
@ -90,17 +99,50 @@ garm-cli init --name=dev --url=https://runner.example.com --username=admin --pas
Token: token.Payload.Token, Token: token.Payload.Token,
}) })
authToken = openapiRuntimeClient.BearerToken(token.Payload.Token)
cfg.ActiveManager = loginProfileName cfg.ActiveManager = loginProfileName
if err := cfg.SaveConfig(); err != nil { if err := cfg.SaveConfig(); err != nil {
return errors.Wrap(err, "saving config") return errors.Wrap(err, "saving config")
} }
renderUserTable(response.Payload) updateUrlsReq := apiClientController.NewUpdateControllerParams()
updateUrlsReq.Body = params.UpdateControllerParams{
MetadataURL: &metadataURL,
CallbackURL: &callbackURL,
WebhookURL: &webhookURL,
}
controllerInfoResponse, err := apiCli.Controller.UpdateController(updateUrlsReq, authToken)
renderResponseMessage(response.Payload, controllerInfoResponse.Payload, err)
return nil return nil
}, },
} }
func ensureDefaultEndpoints(loginURL string) (err error) {
if metadataURL == "" {
metadataURL, err = url.JoinPath(loginURL, "api/v1/callbacks")
if err != nil {
return err
}
}
if callbackURL == "" {
callbackURL, err = url.JoinPath(loginURL, "api/v1/callbacks")
if err != nil {
return err
}
}
if webhookURL == "" {
webhookURL, err = url.JoinPath(loginURL, "webhooks")
if err != nil {
return err
}
}
return nil
}
func promptUnsetInitVariables() error { func promptUnsetInitVariables() error {
var err error var err error
if loginUserName == "" { if loginUserName == "" {
@ -123,6 +165,7 @@ func promptUnsetInitVariables() error {
return err return err
} }
} }
return nil return nil
} }
@ -133,13 +176,16 @@ func init() {
initCmd.Flags().StringVarP(&loginURL, "url", "a", "", "The base URL for the runner manager API") initCmd.Flags().StringVarP(&loginURL, "url", "a", "", "The base URL for the runner manager API")
initCmd.Flags().StringVarP(&loginUserName, "username", "u", "", "The desired administrative username") initCmd.Flags().StringVarP(&loginUserName, "username", "u", "", "The desired administrative username")
initCmd.Flags().StringVarP(&loginEmail, "email", "e", "", "Email address") initCmd.Flags().StringVarP(&loginEmail, "email", "e", "", "Email address")
initCmd.Flags().StringVarP(&metadataURL, "metadata-url", "m", "", "The metadata URL for the controller (ie. https://garm.example.com/api/v1/metadata)")
initCmd.Flags().StringVarP(&callbackURL, "callback-url", "c", "", "The callback URL for the controller (ie. https://garm.example.com/api/v1/callbacks)")
initCmd.Flags().StringVarP(&webhookURL, "webhook-url", "w", "", "The webhook URL for the controller (ie. https://garm.example.com/webhooks)")
initCmd.Flags().StringVarP(&loginFullName, "full-name", "f", "", "Full name of the user") initCmd.Flags().StringVarP(&loginFullName, "full-name", "f", "", "Full name of the user")
initCmd.Flags().StringVarP(&loginPassword, "password", "p", "", "The admin password") initCmd.Flags().StringVarP(&loginPassword, "password", "p", "", "The admin password")
initCmd.MarkFlagRequired("name") //nolint initCmd.MarkFlagRequired("name") //nolint
initCmd.MarkFlagRequired("url") //nolint initCmd.MarkFlagRequired("url") //nolint
} }
func renderUserTable(user params.User) { func renderUserTable(user params.User) string {
t := table.NewWriter() t := table.NewWriter()
header := table.Row{"Field", "Value"} header := table.Row{"Field", "Value"}
t.AppendHeader(header) t.AppendHeader(header)
@ -148,5 +194,54 @@ func renderUserTable(user params.User) {
t.AppendRow(table.Row{"Username", user.Username}) t.AppendRow(table.Row{"Username", user.Username})
t.AppendRow(table.Row{"Email", user.Email}) t.AppendRow(table.Row{"Email", user.Email})
t.AppendRow(table.Row{"Enabled", user.Enabled}) t.AppendRow(table.Row{"Enabled", user.Enabled})
fmt.Println(t.Render()) return t.Render()
}
func renderResponseMessage(user params.User, controllerInfo params.ControllerInfo, err error) {
userTable := renderUserTable(user)
controllerInfoTable := renderControllerInfoTable(controllerInfo)
headerMsg := `Congrats! Your controller is now initialized.
Following are the details of the admin user and details about the controller.
Admin user information:
%s
`
controllerMsg := `Controller information:
%s
Make sure that the URLs in the table above are reachable by the relevant parties.
The metadata and callback URLs *must* be accessible by the runners that GARM spins up.
The base webhook and the controller webhook URLs must be accessible by GitHub or GHES.
`
controllerErrorMsg := `WARNING: Failed to set the required controller URLs with error: %q
Please run:
garm-cli controller show
To make sure that the callback, metadata and webhook URLs are set correctly. If not,
you must set them up by running:
garm-cli controller update \
--metadata-url=<metadata-url> \
--callback-url=<callback-url> \
--webhook-url=<webhook-url>
See the help message for garm-cli controller update for more information.
`
var ctrlMsg string
if err != nil {
ctrlMsg = fmt.Sprintf(controllerErrorMsg, err)
} else {
ctrlMsg = fmt.Sprintf(controllerMsg, controllerInfoTable)
}
fmt.Printf("%s\n%s\n", fmt.Sprintf(headerMsg, userTable), ctrlMsg)
} }

View file

@ -16,6 +16,7 @@ package common
import ( import (
"errors" "errors"
"fmt"
"github.com/manifoldco/promptui" "github.com/manifoldco/promptui"
"github.com/nbutton23/zxcvbn-go" "github.com/nbutton23/zxcvbn-go"
@ -45,7 +46,7 @@ func PromptPassword(label string) (string, error) {
return result, nil return result, nil
} }
func PromptString(label string) (string, error) { func PromptString(label string, a ...interface{}) (string, error) {
validate := func(input string) error { validate := func(input string) error {
if len(input) == 0 { if len(input) == 0 {
return errors.New("empty input not allowed") return errors.New("empty input not allowed")
@ -54,7 +55,7 @@ func PromptString(label string) (string, error) {
} }
prompt := promptui.Prompt{ prompt := promptui.Prompt{
Label: label, Label: fmt.Sprintf(label, a...),
Validate: validate, Validate: validate,
} }
result, err := prompt.Run() result, err := prompt.Run()

View file

@ -41,6 +41,7 @@ import (
"github.com/cloudbase/garm/database" "github.com/cloudbase/garm/database"
"github.com/cloudbase/garm/database/common" "github.com/cloudbase/garm/database/common"
"github.com/cloudbase/garm/metrics" "github.com/cloudbase/garm/metrics"
"github.com/cloudbase/garm/params"
"github.com/cloudbase/garm/runner" //nolint:typecheck "github.com/cloudbase/garm/runner" //nolint:typecheck
runnerMetrics "github.com/cloudbase/garm/runner/metrics" runnerMetrics "github.com/cloudbase/garm/runner/metrics"
garmUtil "github.com/cloudbase/garm/util" garmUtil "github.com/cloudbase/garm/util"
@ -142,6 +143,38 @@ func setupLogging(ctx context.Context, logCfg config.Logging, hub *websocket.Hub
slog.SetDefault(slog.New(wrapped)) slog.SetDefault(slog.New(wrapped))
} }
func maybeUpdateURLsFromConfig(cfg config.Config, store common.Store) error {
info, err := store.ControllerInfo()
if err != nil {
return errors.Wrap(err, "fetching controller info")
}
var updateParams params.UpdateControllerParams
if info.MetadataURL == "" && cfg.Default.MetadataURL != "" {
updateParams.MetadataURL = &cfg.Default.MetadataURL
}
if info.CallbackURL == "" && cfg.Default.CallbackURL != "" {
updateParams.CallbackURL = &cfg.Default.CallbackURL
}
if info.WebhookURL == "" && cfg.Default.WebhookURL != "" {
updateParams.WebhookURL = &cfg.Default.WebhookURL
}
if updateParams.MetadataURL == nil && updateParams.CallbackURL == nil && updateParams.WebhookURL == nil {
// nothing to update
return nil
}
_, err = store.UpdateController(updateParams)
if err != nil {
return errors.Wrap(err, "updating controller info")
}
return nil
}
func main() { func main() {
flag.Parse() flag.Parse()
if *version { if *version {
@ -181,6 +214,10 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
if err := maybeUpdateURLsFromConfig(*cfg, db); err != nil {
log.Fatal(err)
}
runner, err := runner.NewRunner(ctx, *cfg, db) runner, err := runner.NewRunner(ctx, *cfg, db)
if err != nil { if err != nil {
log.Fatalf("failed to create controller: %+v", err) log.Fatalf("failed to create controller: %+v", err)
@ -212,12 +249,17 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
urlsRequiredMiddleware, err := auth.NewUrlsRequiredMiddleware(db)
if err != nil {
log.Fatal(err)
}
metricsMiddleware, err := auth.NewMetricsMiddleware(cfg.JWTAuth) metricsMiddleware, err := auth.NewMetricsMiddleware(cfg.JWTAuth)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
router := routers.NewAPIRouter(controller, jwtMiddleware, initMiddleware, instanceMiddleware, cfg.Default.EnableWebhookManagement) router := routers.NewAPIRouter(controller, jwtMiddleware, initMiddleware, urlsRequiredMiddleware, instanceMiddleware, cfg.Default.EnableWebhookManagement)
// start the metrics collector // start the metrics collector
if cfg.Metrics.Enable { if cfg.Metrics.Enable {

View file

@ -129,6 +129,12 @@ type EntityPools interface {
ListEntityInstances(ctx context.Context, entity params.GithubEntity) ([]params.Instance, error) ListEntityInstances(ctx context.Context, entity params.GithubEntity) ([]params.Instance, error)
} }
type ControllerStore interface {
ControllerInfo() (params.ControllerInfo, error)
InitController() (params.ControllerInfo, error)
UpdateController(info params.UpdateControllerParams) (params.ControllerInfo, error)
}
//go:generate mockery --name=Store //go:generate mockery --name=Store
type Store interface { type Store interface {
RepoStore RepoStore
@ -141,7 +147,5 @@ type Store interface {
EntityPools EntityPools
GithubEndpointStore GithubEndpointStore
GithubCredentialsStore GithubCredentialsStore
ControllerStore
ControllerInfo() (params.ControllerInfo, error)
InitController() (params.ControllerInfo, error)
} }

View file

@ -1516,6 +1516,34 @@ func (_m *Store) UnlockJob(ctx context.Context, jobID int64, entityID string) er
return r0 return r0
} }
// UpdateController provides a mock function with given fields: info
func (_m *Store) UpdateController(info params.UpdateControllerParams) (params.ControllerInfo, error) {
ret := _m.Called(info)
if len(ret) == 0 {
panic("no return value specified for UpdateController")
}
var r0 params.ControllerInfo
var r1 error
if rf, ok := ret.Get(0).(func(params.UpdateControllerParams) (params.ControllerInfo, error)); ok {
return rf(info)
}
if rf, ok := ret.Get(0).(func(params.UpdateControllerParams) params.ControllerInfo); ok {
r0 = rf(info)
} else {
r0 = ret.Get(0).(params.ControllerInfo)
}
if rf, ok := ret.Get(1).(func(params.UpdateControllerParams) error); ok {
r1 = rf(info)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateEnterprise provides a mock function with given fields: ctx, enterpriseID, param // UpdateEnterprise provides a mock function with given fields: ctx, enterpriseID, param
func (_m *Store) UpdateEnterprise(ctx context.Context, enterpriseID string, param params.UpdateEntityParams) (params.Enterprise, error) { func (_m *Store) UpdateEnterprise(ctx context.Context, enterpriseID string, param params.UpdateEntityParams) (params.Enterprise, error) {
ret := _m.Called(ctx, enterpriseID, param) ret := _m.Called(ctx, enterpriseID, param)

View file

@ -15,6 +15,8 @@
package sql package sql
import ( import (
"net/url"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"
"gorm.io/gorm" "gorm.io/gorm"
@ -23,6 +25,21 @@ import (
"github.com/cloudbase/garm/params" "github.com/cloudbase/garm/params"
) )
func dbControllerToCommonController(dbInfo ControllerInfo) (params.ControllerInfo, error) {
url, err := url.JoinPath(dbInfo.WebhookBaseURL, dbInfo.ControllerID.String())
if err != nil {
return params.ControllerInfo{}, errors.Wrap(err, "joining webhook URL")
}
return params.ControllerInfo{
ControllerID: dbInfo.ControllerID,
MetadataURL: dbInfo.MetadataURL,
WebhookURL: dbInfo.WebhookBaseURL,
ControllerWebhookURL: url,
CallbackURL: dbInfo.CallbackURL,
}, nil
}
func (s *sqlDatabase) ControllerInfo() (params.ControllerInfo, error) { func (s *sqlDatabase) ControllerInfo() (params.ControllerInfo, error) {
var info ControllerInfo var info ControllerInfo
q := s.conn.Model(&ControllerInfo{}).First(&info) q := s.conn.Model(&ControllerInfo{}).First(&info)
@ -32,9 +49,13 @@ func (s *sqlDatabase) ControllerInfo() (params.ControllerInfo, error) {
} }
return params.ControllerInfo{}, errors.Wrap(q.Error, "fetching controller info") return params.ControllerInfo{}, errors.Wrap(q.Error, "fetching controller info")
} }
return params.ControllerInfo{
ControllerID: info.ControllerID, paramInfo, err := dbControllerToCommonController(info)
}, nil if err != nil {
return params.ControllerInfo{}, errors.Wrap(err, "converting controller info")
}
return paramInfo, nil
} }
func (s *sqlDatabase) InitController() (params.ControllerInfo, error) { func (s *sqlDatabase) InitController() (params.ControllerInfo, error) {
@ -60,3 +81,41 @@ func (s *sqlDatabase) InitController() (params.ControllerInfo, error) {
ControllerID: newInfo.ControllerID, ControllerID: newInfo.ControllerID,
}, nil }, nil
} }
func (s *sqlDatabase) UpdateController(info params.UpdateControllerParams) (params.ControllerInfo, error) {
var dbInfo ControllerInfo
q := s.conn.Model(&ControllerInfo{}).First(&dbInfo)
if q.Error != nil {
if errors.Is(q.Error, gorm.ErrRecordNotFound) {
return params.ControllerInfo{}, errors.Wrap(runnerErrors.ErrNotFound, "fetching controller info")
}
return params.ControllerInfo{}, errors.Wrap(q.Error, "fetching controller info")
}
if err := info.Validate(); err != nil {
return params.ControllerInfo{}, errors.Wrap(err, "validating controller info")
}
if info.MetadataURL != nil {
dbInfo.MetadataURL = *info.MetadataURL
}
if info.CallbackURL != nil {
dbInfo.CallbackURL = *info.CallbackURL
}
if info.WebhookURL != nil {
dbInfo.WebhookBaseURL = *info.WebhookURL
}
q = s.conn.Save(&dbInfo)
if q.Error != nil {
return params.ControllerInfo{}, errors.Wrap(q.Error, "saving controller info")
}
paramInfo, err := dbControllerToCommonController(dbInfo)
if err != nil {
return params.ControllerInfo{}, errors.Wrap(err, "converting controller info")
}
return paramInfo, nil
}

View file

@ -207,6 +207,10 @@ type ControllerInfo struct {
Base Base
ControllerID uuid.UUID ControllerID uuid.UUID
CallbackURL string
MetadataURL string
WebhookBaseURL string
} }
type WorkflowJob struct { type WorkflowJob struct {

View file

@ -501,3 +501,34 @@ func (u UpdateGithubCredentialsParams) Validate() error {
return nil return nil
} }
type UpdateControllerParams struct {
MetadataURL *string `json:"metadata_url,omitempty"`
CallbackURL *string `json:"callback_url,omitempty"`
WebhookURL *string `json:"webhook_url,omitempty"`
}
func (u UpdateControllerParams) Validate() error {
if u.MetadataURL != nil {
u, err := url.Parse(*u.MetadataURL)
if err != nil || u.Scheme == "" || u.Host == "" {
return runnerErrors.NewBadRequestError("invalid metadata_url")
}
}
if u.CallbackURL != nil {
u, err := url.Parse(*u.CallbackURL)
if err != nil || u.Scheme == "" || u.Host == "" {
return runnerErrors.NewBadRequestError("invalid callback_url")
}
}
if u.WebhookURL != nil {
u, err := url.Parse(*u.WebhookURL)
if err != nil || u.Scheme == "" || u.Host == "" {
return runnerErrors.NewBadRequestError("invalid webhook_url")
}
}
return nil
}

View file

@ -29,7 +29,6 @@ import (
"sync" "sync"
"time" "time"
"github.com/google/uuid"
"github.com/juju/clock" "github.com/juju/clock"
"github.com/juju/retry" "github.com/juju/retry"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -65,7 +64,6 @@ func NewRunner(ctx context.Context, cfg config.Config, db dbCommon.Store) (*Runn
} }
poolManagerCtrl := &poolManagerCtrl{ poolManagerCtrl := &poolManagerCtrl{
controllerID: ctrlID.ControllerID.String(),
config: cfg, config: cfg,
store: db, store: db,
repositories: map[string]common.PoolManager{}, repositories: map[string]common.PoolManager{},
@ -78,7 +76,6 @@ func NewRunner(ctx context.Context, cfg config.Config, db dbCommon.Store) (*Runn
store: db, store: db,
poolManagerCtrl: poolManagerCtrl, poolManagerCtrl: poolManagerCtrl,
providers: providers, providers: providers,
controllerID: ctrlID.ControllerID,
} }
if err := runner.loadReposOrgsAndEnterprises(); err != nil { if err := runner.loadReposOrgsAndEnterprises(); err != nil {
@ -91,9 +88,8 @@ func NewRunner(ctx context.Context, cfg config.Config, db dbCommon.Store) (*Runn
type poolManagerCtrl struct { type poolManagerCtrl struct {
mux sync.Mutex mux sync.Mutex
controllerID string config config.Config
config config.Config store dbCommon.Store
store dbCommon.Store
repositories map[string]common.PoolManager repositories map[string]common.PoolManager
organizations map[string]common.PoolManager organizations map[string]common.PoolManager
@ -345,17 +341,17 @@ func (p *poolManagerCtrl) GetEnterprisePoolManagers() (map[string]common.PoolMan
} }
func (p *poolManagerCtrl) getInternalConfig(_ context.Context, creds params.GithubCredentials, poolBalancerType params.PoolBalancerType) (params.Internal, error) { func (p *poolManagerCtrl) getInternalConfig(_ context.Context, creds params.GithubCredentials, poolBalancerType params.PoolBalancerType) (params.Internal, error) {
var controllerWebhookURL string controllerInfo, err := p.store.ControllerInfo()
if p.config.Default.WebhookURL != "" { if err != nil {
controllerWebhookURL = fmt.Sprintf("%s/%s", p.config.Default.WebhookURL, p.controllerID) return params.Internal{}, errors.Wrap(err, "fetching controller info")
} }
return params.Internal{ return params.Internal{
ControllerID: p.controllerID, ControllerID: controllerInfo.ControllerID.String(),
InstanceCallbackURL: p.config.Default.CallbackURL, InstanceCallbackURL: controllerInfo.CallbackURL,
InstanceMetadataURL: p.config.Default.MetadataURL, InstanceMetadataURL: controllerInfo.MetadataURL,
BaseWebhookURL: p.config.Default.WebhookURL, BaseWebhookURL: controllerInfo.WebhookURL,
ControllerWebhookURL: controllerWebhookURL, ControllerWebhookURL: controllerInfo.ControllerWebhookURL,
JWTSecret: p.config.JWTAuth.Secret, JWTSecret: p.config.JWTAuth.Secret,
PoolBalancerType: poolBalancerType, PoolBalancerType: poolBalancerType,
GithubCredentialsDetails: creds, GithubCredentialsDetails: creds,
@ -372,9 +368,23 @@ type Runner struct {
poolManagerCtrl PoolManagerController poolManagerCtrl PoolManagerController
providers map[string]common.Provider providers map[string]common.Provider
}
controllerInfo params.ControllerInfo // UpdateController will update the controller settings.
controllerID uuid.UUID func (r *Runner) UpdateController(ctx context.Context, param params.UpdateControllerParams) (params.ControllerInfo, error) {
if !auth.IsAdmin(ctx) {
return params.ControllerInfo{}, runnerErrors.ErrUnauthorized
}
if err := param.Validate(); err != nil {
return params.ControllerInfo{}, errors.Wrap(err, "validating controller update params")
}
info, err := r.store.UpdateController(param)
if err != nil {
return params.ControllerInfo{}, errors.Wrap(err, "updating controller info")
}
return info, nil
} }
// GetControllerInfo returns the controller id and the hostname. // GetControllerInfo returns the controller id and the hostname.
@ -408,19 +418,18 @@ func (r *Runner) GetControllerInfo(ctx context.Context) (params.ControllerInfo,
if err != nil { if err != nil {
return params.ControllerInfo{}, errors.Wrap(err, "fetching hostname") return params.ControllerInfo{}, errors.Wrap(err, "fetching hostname")
} }
r.controllerInfo.Hostname = hostname
var controllerWebhook string info, err := r.store.ControllerInfo()
if r.controllerID != uuid.Nil && r.config.Default.WebhookURL != "" { if err != nil {
controllerWebhook = fmt.Sprintf("%s/%s", r.config.Default.WebhookURL, r.controllerID.String()) return params.ControllerInfo{}, errors.Wrap(err, "fetching controller info")
} }
return params.ControllerInfo{
ControllerID: r.controllerID, // This is temporary. Right now, GARM is a single-instance deployment. When we add the
Hostname: hostname, // ability to scale out, the hostname field will be moved form here to a dedicated node
MetadataURL: r.config.Default.MetadataURL, // object. As a single controller will be made up of multiple nodes, we will need to model
CallbackURL: r.config.Default.CallbackURL, // that aspect of GARM.
WebhookURL: r.config.Default.WebhookURL, info.Hostname = hostname
ControllerWebhookURL: controllerWebhook, return info, nil
}, nil
} }
func (r *Runner) ListProviders(ctx context.Context) ([]params.Provider, error) { func (r *Runner) ListProviders(ctx context.Context) ([]params.Provider, error) {

45
testdata/config.toml vendored
View file

@ -249,48 +249,3 @@ disable_jit_config = false
# anything (bash, a binary, python, etc). See documentation in this repo on how to write an # anything (bash, a binary, python, etc). See documentation in this repo on how to write an
# external provider. # external provider.
provider_executable = "/etc/garm/providers.d/azure/garm-external-provider" provider_executable = "/etc/garm/providers.d/azure/garm-external-provider"
# This is a list of credentials that you can define as part of the repository
# or organization definitions. They are not saved inside the database, as there
# is no Vault integration (yet). This will change in the future.
# Credentials defined here can be listed using the API. Obviously, only the name
# and descriptions are returned.
[[github]]
name = "gabriel"
description = "github token or user gabriel"
# This is the type of authentication to use. It can be "pat" or "app"
auth_type = "pat"
[github.pat]
# This is a personal token with access to the repositories and organizations
# you plan on adding to garm. The "workflow" option needs to be selected in order
# to work with repositories, and the admin:org needs to be set if you plan on
# adding an organization.
oauth2_token = "super secret token"
[github.app]
# This is the app_id of the GitHub App that you want to use to authenticate
# with the GitHub API.
# This needs to be changed
app_id = 1
# This is the private key path of the GitHub App that you want to use to authenticate
# with the GitHub API.
# This needs to be changed
private_key_path = "/etc/garm/yourAppName.2024-03-01.private-key.pem"
# This is the installation_id of the GitHub App that you want to use to authenticate
# with the GitHub API.
# This needs to be changed
installation_id = 99
# base_url (optional) is the URL at which your GitHub Enterprise Server can be accessed.
# If these credentials are for github.com, leave this setting blank
base_url = "https://ghe.example.com"
# api_base_url (optional) is the base URL where the GitHub Enterprise Server API can be accessed.
# Leave this blank if these credentials are for github.com.
api_base_url = "https://ghe.example.com"
# upload_base_url (optional) is the base URL where the GitHub Enterprise Server upload API can be accessed.
# Leave this blank if these credentials are for github.com, or if you don't have a separate URL
# for the upload API.
upload_base_url = "https://api.ghe.example.com"
# ca_cert_bundle (optional) is the CA certificate bundle in PEM format that will be used by the github
# client to talk to the API. This bundle will also be sent to all runners as bootstrap params.
# Use this option if you're using a self signed certificate.
# Leave this blank if you're using github.com or if your certificate is signed by a valid CA.
ca_cert_bundle = "/etc/garm/ghe.crt"