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:
parent
7ee235aeb0
commit
9748aa47af
22 changed files with 1067 additions and 177 deletions
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,4 +36,9 @@ var (
|
|||
Error: "init_required",
|
||||
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",
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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.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.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.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(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
|
||||
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")
|
||||
|
||||
// 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 //
|
||||
//////////////////////
|
||||
|
|
|
|||
|
|
@ -278,3 +278,10 @@ definitions:
|
|||
import:
|
||||
package: github.com/cloudbase/garm/params
|
||||
alias: garm_params
|
||||
UpdateControllerParams:
|
||||
type: object
|
||||
x-go-type:
|
||||
type: UpdateControllerParams
|
||||
import:
|
||||
package: github.com/cloudbase/garm/params
|
||||
alias: garm_params
|
||||
|
|
|
|||
|
|
@ -244,6 +244,13 @@ definitions:
|
|||
alias: garm_params
|
||||
package: github.com/cloudbase/garm/params
|
||||
type: Repository
|
||||
UpdateControllerParams:
|
||||
type: object
|
||||
x-go-type:
|
||||
import:
|
||||
alias: garm_params
|
||||
package: github.com/cloudbase/garm/params
|
||||
type: UpdateControllerParams
|
||||
UpdateEntityParams:
|
||||
type: object
|
||||
x-go-type:
|
||||
|
|
@ -311,6 +318,30 @@ paths:
|
|||
summary: Logs in a user and returns a JWT token.
|
||||
tags:
|
||||
- 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:
|
||||
get:
|
||||
operationId: ControllerInfo
|
||||
|
|
|
|||
|
|
@ -51,3 +51,30 @@ func (i *initRequired) Middleware(next http.Handler) http.Handler {
|
|||
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))
|
||||
})
|
||||
}
|
||||
|
|
|
|||
106
client/controller/controller_client.go
Normal file
106
client/controller/controller_client.go
Normal 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
|
||||
}
|
||||
151
client/controller/update_controller_parameters.go
Normal file
151
client/controller/update_controller_parameters.go
Normal 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
|
||||
}
|
||||
179
client/controller/update_controller_responses.go
Normal file
179
client/controller/update_controller_responses.go
Normal 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
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import (
|
|||
httptransport "github.com/go-openapi/runtime/client"
|
||||
"github.com/go-openapi/strfmt"
|
||||
|
||||
"github.com/cloudbase/garm/client/controller"
|
||||
"github.com/cloudbase/garm/client/controller_info"
|
||||
"github.com/cloudbase/garm/client/credentials"
|
||||
"github.com/cloudbase/garm/client/endpoints"
|
||||
|
|
@ -67,6 +68,7 @@ func New(transport runtime.ClientTransport, formats strfmt.Registry) *GarmAPI {
|
|||
|
||||
cli := new(GarmAPI)
|
||||
cli.Transport = transport
|
||||
cli.Controller = controller.New(transport, formats)
|
||||
cli.ControllerInfo = controller_info.New(transport, formats)
|
||||
cli.Credentials = credentials.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
|
||||
type GarmAPI struct {
|
||||
Controller controller.ClientService
|
||||
|
||||
ControllerInfo controller_info.ClientService
|
||||
|
||||
Credentials credentials.ClientService
|
||||
|
|
@ -156,6 +160,7 @@ type GarmAPI struct {
|
|||
// SetTransport changes the transport on the client and all its subresources
|
||||
func (c *GarmAPI) SetTransport(transport runtime.ClientTransport) {
|
||||
c.Transport = transport
|
||||
c.Controller.SetTransport(transport)
|
||||
c.ControllerInfo.SetTransport(transport)
|
||||
c.Credentials.SetTransport(transport)
|
||||
c.Endpoints.SetTransport(transport)
|
||||
|
|
|
|||
173
cmd/garm-cli/cmd/controller.go
Normal file
173
cmd/garm-cli/cmd/controller.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -16,12 +16,15 @@ package cmd
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
openapiRuntimeClient "github.com/go-openapi/runtime/client"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
apiClientController "github.com/cloudbase/garm/client/controller"
|
||||
apiClientFirstRun "github.com/cloudbase/garm/client/first_run"
|
||||
apiClientLogin "github.com/cloudbase/garm/client/login"
|
||||
"github.com/cloudbase/garm/cmd/garm-cli/common"
|
||||
|
|
@ -29,6 +32,12 @@ import (
|
|||
"github.com/cloudbase/garm/params"
|
||||
)
|
||||
|
||||
var (
|
||||
callbackURL string
|
||||
metadataURL string
|
||||
webhookURL string
|
||||
)
|
||||
|
||||
// initCmd represents the init command
|
||||
var initCmd = &cobra.Command{
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
ensureDefaultEndpoints(url)
|
||||
|
||||
newUserReq := apiClientFirstRun.NewFirstRunParams()
|
||||
newUserReq.Body = params.NewUserParams{
|
||||
Username: loginUserName,
|
||||
|
|
@ -63,9 +75,6 @@ garm-cli init --name=dev --url=https://runner.example.com --username=admin --pas
|
|||
FullName: loginFullName,
|
||||
Email: loginEmail,
|
||||
}
|
||||
|
||||
url := strings.TrimSuffix(loginURL, "/")
|
||||
|
||||
initAPIClient(url, "")
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
authToken = openapiRuntimeClient.BearerToken(token.Payload.Token)
|
||||
cfg.ActiveManager = loginProfileName
|
||||
|
||||
if err := cfg.SaveConfig(); err != nil {
|
||||
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
|
||||
},
|
||||
}
|
||||
|
||||
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 {
|
||||
var err error
|
||||
if loginUserName == "" {
|
||||
|
|
@ -123,6 +165,7 @@ func promptUnsetInitVariables() error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
|
||||
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(&loginUserName, "username", "u", "", "The desired administrative username")
|
||||
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(&loginPassword, "password", "p", "", "The admin password")
|
||||
initCmd.MarkFlagRequired("name") //nolint
|
||||
initCmd.MarkFlagRequired("url") //nolint
|
||||
}
|
||||
|
||||
func renderUserTable(user params.User) {
|
||||
func renderUserTable(user params.User) string {
|
||||
t := table.NewWriter()
|
||||
header := table.Row{"Field", "Value"}
|
||||
t.AppendHeader(header)
|
||||
|
|
@ -148,5 +194,54 @@ func renderUserTable(user params.User) {
|
|||
t.AppendRow(table.Row{"Username", user.Username})
|
||||
t.AppendRow(table.Row{"Email", user.Email})
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ package common
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/nbutton23/zxcvbn-go"
|
||||
|
|
@ -45,7 +46,7 @@ func PromptPassword(label string) (string, error) {
|
|||
return result, nil
|
||||
}
|
||||
|
||||
func PromptString(label string) (string, error) {
|
||||
func PromptString(label string, a ...interface{}) (string, error) {
|
||||
validate := func(input string) error {
|
||||
if len(input) == 0 {
|
||||
return errors.New("empty input not allowed")
|
||||
|
|
@ -54,7 +55,7 @@ func PromptString(label string) (string, error) {
|
|||
}
|
||||
|
||||
prompt := promptui.Prompt{
|
||||
Label: label,
|
||||
Label: fmt.Sprintf(label, a...),
|
||||
Validate: validate,
|
||||
}
|
||||
result, err := prompt.Run()
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import (
|
|||
"github.com/cloudbase/garm/database"
|
||||
"github.com/cloudbase/garm/database/common"
|
||||
"github.com/cloudbase/garm/metrics"
|
||||
"github.com/cloudbase/garm/params"
|
||||
"github.com/cloudbase/garm/runner" //nolint:typecheck
|
||||
runnerMetrics "github.com/cloudbase/garm/runner/metrics"
|
||||
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))
|
||||
}
|
||||
|
||||
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() {
|
||||
flag.Parse()
|
||||
if *version {
|
||||
|
|
@ -181,6 +214,10 @@ func main() {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := maybeUpdateURLsFromConfig(*cfg, db); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
runner, err := runner.NewRunner(ctx, *cfg, db)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create controller: %+v", err)
|
||||
|
|
@ -212,12 +249,17 @@ func main() {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
urlsRequiredMiddleware, err := auth.NewUrlsRequiredMiddleware(db)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
metricsMiddleware, err := auth.NewMetricsMiddleware(cfg.JWTAuth)
|
||||
if err != nil {
|
||||
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
|
||||
if cfg.Metrics.Enable {
|
||||
|
|
|
|||
|
|
@ -129,6 +129,12 @@ type EntityPools interface {
|
|||
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
|
||||
type Store interface {
|
||||
RepoStore
|
||||
|
|
@ -141,7 +147,5 @@ type Store interface {
|
|||
EntityPools
|
||||
GithubEndpointStore
|
||||
GithubCredentialsStore
|
||||
|
||||
ControllerInfo() (params.ControllerInfo, error)
|
||||
InitController() (params.ControllerInfo, error)
|
||||
ControllerStore
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1516,6 +1516,34 @@ func (_m *Store) UnlockJob(ctx context.Context, jobID int64, entityID string) er
|
|||
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
|
||||
func (_m *Store) UpdateEnterprise(ctx context.Context, enterpriseID string, param params.UpdateEntityParams) (params.Enterprise, error) {
|
||||
ret := _m.Called(ctx, enterpriseID, param)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
package sql
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
|
|
@ -23,6 +25,21 @@ import (
|
|||
"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) {
|
||||
var info ControllerInfo
|
||||
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{
|
||||
ControllerID: info.ControllerID,
|
||||
}, nil
|
||||
|
||||
paramInfo, err := dbControllerToCommonController(info)
|
||||
if err != nil {
|
||||
return params.ControllerInfo{}, errors.Wrap(err, "converting controller info")
|
||||
}
|
||||
|
||||
return paramInfo, nil
|
||||
}
|
||||
|
||||
func (s *sqlDatabase) InitController() (params.ControllerInfo, error) {
|
||||
|
|
@ -60,3 +81,41 @@ func (s *sqlDatabase) InitController() (params.ControllerInfo, error) {
|
|||
ControllerID: newInfo.ControllerID,
|
||||
}, 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -207,6 +207,10 @@ type ControllerInfo struct {
|
|||
Base
|
||||
|
||||
ControllerID uuid.UUID
|
||||
|
||||
CallbackURL string
|
||||
MetadataURL string
|
||||
WebhookBaseURL string
|
||||
}
|
||||
|
||||
type WorkflowJob struct {
|
||||
|
|
|
|||
|
|
@ -501,3 +501,34 @@ func (u UpdateGithubCredentialsParams) Validate() error {
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/juju/clock"
|
||||
"github.com/juju/retry"
|
||||
"github.com/pkg/errors"
|
||||
|
|
@ -65,7 +64,6 @@ func NewRunner(ctx context.Context, cfg config.Config, db dbCommon.Store) (*Runn
|
|||
}
|
||||
|
||||
poolManagerCtrl := &poolManagerCtrl{
|
||||
controllerID: ctrlID.ControllerID.String(),
|
||||
config: cfg,
|
||||
store: db,
|
||||
repositories: map[string]common.PoolManager{},
|
||||
|
|
@ -78,7 +76,6 @@ func NewRunner(ctx context.Context, cfg config.Config, db dbCommon.Store) (*Runn
|
|||
store: db,
|
||||
poolManagerCtrl: poolManagerCtrl,
|
||||
providers: providers,
|
||||
controllerID: ctrlID.ControllerID,
|
||||
}
|
||||
|
||||
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 {
|
||||
mux sync.Mutex
|
||||
|
||||
controllerID string
|
||||
config config.Config
|
||||
store dbCommon.Store
|
||||
config config.Config
|
||||
store dbCommon.Store
|
||||
|
||||
repositories 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) {
|
||||
var controllerWebhookURL string
|
||||
if p.config.Default.WebhookURL != "" {
|
||||
controllerWebhookURL = fmt.Sprintf("%s/%s", p.config.Default.WebhookURL, p.controllerID)
|
||||
controllerInfo, err := p.store.ControllerInfo()
|
||||
if err != nil {
|
||||
return params.Internal{}, errors.Wrap(err, "fetching controller info")
|
||||
}
|
||||
|
||||
return params.Internal{
|
||||
ControllerID: p.controllerID,
|
||||
InstanceCallbackURL: p.config.Default.CallbackURL,
|
||||
InstanceMetadataURL: p.config.Default.MetadataURL,
|
||||
BaseWebhookURL: p.config.Default.WebhookURL,
|
||||
ControllerWebhookURL: controllerWebhookURL,
|
||||
ControllerID: controllerInfo.ControllerID.String(),
|
||||
InstanceCallbackURL: controllerInfo.CallbackURL,
|
||||
InstanceMetadataURL: controllerInfo.MetadataURL,
|
||||
BaseWebhookURL: controllerInfo.WebhookURL,
|
||||
ControllerWebhookURL: controllerInfo.ControllerWebhookURL,
|
||||
JWTSecret: p.config.JWTAuth.Secret,
|
||||
PoolBalancerType: poolBalancerType,
|
||||
GithubCredentialsDetails: creds,
|
||||
|
|
@ -372,9 +368,23 @@ type Runner struct {
|
|||
poolManagerCtrl PoolManagerController
|
||||
|
||||
providers map[string]common.Provider
|
||||
}
|
||||
|
||||
controllerInfo params.ControllerInfo
|
||||
controllerID uuid.UUID
|
||||
// UpdateController will update the controller settings.
|
||||
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.
|
||||
|
|
@ -408,19 +418,18 @@ func (r *Runner) GetControllerInfo(ctx context.Context) (params.ControllerInfo,
|
|||
if err != nil {
|
||||
return params.ControllerInfo{}, errors.Wrap(err, "fetching hostname")
|
||||
}
|
||||
r.controllerInfo.Hostname = hostname
|
||||
var controllerWebhook string
|
||||
if r.controllerID != uuid.Nil && r.config.Default.WebhookURL != "" {
|
||||
controllerWebhook = fmt.Sprintf("%s/%s", r.config.Default.WebhookURL, r.controllerID.String())
|
||||
|
||||
info, err := r.store.ControllerInfo()
|
||||
if err != nil {
|
||||
return params.ControllerInfo{}, errors.Wrap(err, "fetching controller info")
|
||||
}
|
||||
return params.ControllerInfo{
|
||||
ControllerID: r.controllerID,
|
||||
Hostname: hostname,
|
||||
MetadataURL: r.config.Default.MetadataURL,
|
||||
CallbackURL: r.config.Default.CallbackURL,
|
||||
WebhookURL: r.config.Default.WebhookURL,
|
||||
ControllerWebhookURL: controllerWebhook,
|
||||
}, nil
|
||||
|
||||
// This is temporary. Right now, GARM is a single-instance deployment. When we add the
|
||||
// ability to scale out, the hostname field will be moved form here to a dedicated node
|
||||
// object. As a single controller will be made up of multiple nodes, we will need to model
|
||||
// that aspect of GARM.
|
||||
info.Hostname = hostname
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (r *Runner) ListProviders(ctx context.Context) ([]params.Provider, error) {
|
||||
|
|
|
|||
45
testdata/config.toml
vendored
45
testdata/config.toml
vendored
|
|
@ -249,48 +249,3 @@ disable_jit_config = false
|
|||
# anything (bash, a binary, python, etc). See documentation in this repo on how to write an
|
||||
# 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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue