Use separate endpoints to list tools

Users and instances now have different endpoint for listing tools.
Moreover, users can now use a flag to see what tools are available
upstream if sync is off:

garm-cli controller tools list --upstream

Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
This commit is contained in:
Gabriel Adrian Samfira 2026-02-08 15:53:05 +02:00 committed by Gabriel
parent 9a9080c180
commit 61b4b4cadd
12 changed files with 828 additions and 440 deletions

View file

@ -85,7 +85,65 @@ func (a *APIController) InstanceGARMToolsHandler(w http.ResponseWriter, r *http.
}
}
tools, err := a.r.GetGARMTools(ctx, uint64(pageLocation), uint64(pageSize))
tools, err := a.r.GetAgentGARMTools(ctx, uint64(pageLocation), uint64(pageSize))
if err != nil {
handleError(ctx, w, err)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(tools); err != nil {
slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response")
}
}
// swagger:route GET /tools/garm-agent tools AdminGarmAgentList
//
// List GARM agent tools for admin users.
//
// Parameters:
// + name: page
// description: The page at which to list.
// type: integer
// in: query
// required: false
// + name: pageSize
// description: Number of items per page.
// type: integer
// in: query
// required: false
// + name: upstream
// description: If true, list tools from the upstream cached release instead of the local object store.
// type: boolean
// in: query
// required: false
//
// Responses:
// 200: GARMAgentToolsPaginatedResponse
// 400: APIErrorResponse
func (a *APIController) AdminGARMToolsHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var pageLocation int64
var pageSize int64 = 25
pageArg := r.URL.Query().Get("page")
pageSizeArg := r.URL.Query().Get("pageSize")
if pageArg != "" {
pageInt, err := strconv.ParseInt(pageArg, 10, 64)
if err == nil && pageInt >= 0 {
pageLocation = pageInt
}
}
if pageSizeArg != "" {
pageSizeInt, err := strconv.ParseInt(pageSizeArg, 10, 64)
if err == nil && pageSizeInt >= 0 {
pageSize = pageSizeInt
}
}
upstream := r.URL.Query().Get("upstream") == "true"
tools, err := a.r.GetGARMTools(ctx, uint64(pageLocation), uint64(pageSize), upstream)
if err != nil {
handleError(ctx, w, err)
return

View file

@ -267,8 +267,8 @@ func NewAPIRouter(han *controllers.APIController, authMiddleware, initMiddleware
///////////////////////////////////////////////////////
// Tools URLs (garm agent, cached gitea runner, etc) //
///////////////////////////////////////////////////////
apiRouter.Handle("/tools/garm-agent/", http.HandlerFunc(han.InstanceGARMToolsHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/tools/garm-agent", http.HandlerFunc(han.InstanceGARMToolsHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/tools/garm-agent/", http.HandlerFunc(han.AdminGARMToolsHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/tools/garm-agent", http.HandlerFunc(han.AdminGARMToolsHandler)).Methods("GET", "OPTIONS")
// Upload garm agent tool
apiRouter.Handle("/tools/garm-agent/", http.HandlerFunc(han.UploadGARMAgentToolHandler)).Methods("POST", "OPTIONS")
apiRouter.Handle("/tools/garm-agent", http.HandlerFunc(han.UploadGARMAgentToolHandler)).Methods("POST", "OPTIONS")

View file

@ -2603,7 +2603,7 @@ paths:
- templates
/tools/garm-agent:
get:
operationId: GarmAgentList
operationId: AdminGarmAgentList
parameters:
- description: The page at which to list.
in: query
@ -2613,6 +2613,10 @@ paths:
in: query
name: pageSize
type: integer
- description: If true, list tools from the upstream cached release instead of the local object store.
in: query
name: upstream
type: boolean
responses:
"200":
description: GARMAgentToolsPaginatedResponse
@ -2622,7 +2626,7 @@ paths:
description: APIErrorResponse
schema:
$ref: '#/definitions/APIErrorResponse'
summary: List GARM agent tools.
summary: List GARM agent tools for admin users.
tags:
- tools
post:

View file

@ -0,0 +1,232 @@
// Code generated by go-swagger; DO NOT EDIT.
package tools
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"net/http"
"time"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
cr "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// NewAdminGarmAgentListParams creates a new AdminGarmAgentListParams 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 NewAdminGarmAgentListParams() *AdminGarmAgentListParams {
return &AdminGarmAgentListParams{
timeout: cr.DefaultTimeout,
}
}
// NewAdminGarmAgentListParamsWithTimeout creates a new AdminGarmAgentListParams object
// with the ability to set a timeout on a request.
func NewAdminGarmAgentListParamsWithTimeout(timeout time.Duration) *AdminGarmAgentListParams {
return &AdminGarmAgentListParams{
timeout: timeout,
}
}
// NewAdminGarmAgentListParamsWithContext creates a new AdminGarmAgentListParams object
// with the ability to set a context for a request.
func NewAdminGarmAgentListParamsWithContext(ctx context.Context) *AdminGarmAgentListParams {
return &AdminGarmAgentListParams{
Context: ctx,
}
}
// NewAdminGarmAgentListParamsWithHTTPClient creates a new AdminGarmAgentListParams object
// with the ability to set a custom HTTPClient for a request.
func NewAdminGarmAgentListParamsWithHTTPClient(client *http.Client) *AdminGarmAgentListParams {
return &AdminGarmAgentListParams{
HTTPClient: client,
}
}
/*
AdminGarmAgentListParams contains all the parameters to send to the API endpoint
for the admin garm agent list operation.
Typically these are written to a http.Request.
*/
type AdminGarmAgentListParams struct {
/* Page.
The page at which to list.
*/
Page *int64
/* PageSize.
Number of items per page.
*/
PageSize *int64
/* Upstream.
If true, list tools from the upstream cached release instead of the local object store.
*/
Upstream *bool
timeout time.Duration
Context context.Context
HTTPClient *http.Client
}
// WithDefaults hydrates default values in the admin garm agent list params (not the query body).
//
// All values with no default are reset to their zero value.
func (o *AdminGarmAgentListParams) WithDefaults() *AdminGarmAgentListParams {
o.SetDefaults()
return o
}
// SetDefaults hydrates default values in the admin garm agent list params (not the query body).
//
// All values with no default are reset to their zero value.
func (o *AdminGarmAgentListParams) SetDefaults() {
// no default values defined for this parameter
}
// WithTimeout adds the timeout to the admin garm agent list params
func (o *AdminGarmAgentListParams) WithTimeout(timeout time.Duration) *AdminGarmAgentListParams {
o.SetTimeout(timeout)
return o
}
// SetTimeout adds the timeout to the admin garm agent list params
func (o *AdminGarmAgentListParams) SetTimeout(timeout time.Duration) {
o.timeout = timeout
}
// WithContext adds the context to the admin garm agent list params
func (o *AdminGarmAgentListParams) WithContext(ctx context.Context) *AdminGarmAgentListParams {
o.SetContext(ctx)
return o
}
// SetContext adds the context to the admin garm agent list params
func (o *AdminGarmAgentListParams) SetContext(ctx context.Context) {
o.Context = ctx
}
// WithHTTPClient adds the HTTPClient to the admin garm agent list params
func (o *AdminGarmAgentListParams) WithHTTPClient(client *http.Client) *AdminGarmAgentListParams {
o.SetHTTPClient(client)
return o
}
// SetHTTPClient adds the HTTPClient to the admin garm agent list params
func (o *AdminGarmAgentListParams) SetHTTPClient(client *http.Client) {
o.HTTPClient = client
}
// WithPage adds the page to the admin garm agent list params
func (o *AdminGarmAgentListParams) WithPage(page *int64) *AdminGarmAgentListParams {
o.SetPage(page)
return o
}
// SetPage adds the page to the admin garm agent list params
func (o *AdminGarmAgentListParams) SetPage(page *int64) {
o.Page = page
}
// WithPageSize adds the pageSize to the admin garm agent list params
func (o *AdminGarmAgentListParams) WithPageSize(pageSize *int64) *AdminGarmAgentListParams {
o.SetPageSize(pageSize)
return o
}
// SetPageSize adds the pageSize to the admin garm agent list params
func (o *AdminGarmAgentListParams) SetPageSize(pageSize *int64) {
o.PageSize = pageSize
}
// WithUpstream adds the upstream to the admin garm agent list params
func (o *AdminGarmAgentListParams) WithUpstream(upstream *bool) *AdminGarmAgentListParams {
o.SetUpstream(upstream)
return o
}
// SetUpstream adds the upstream to the admin garm agent list params
func (o *AdminGarmAgentListParams) SetUpstream(upstream *bool) {
o.Upstream = upstream
}
// WriteToRequest writes these params to a swagger request
func (o *AdminGarmAgentListParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
if err := r.SetTimeout(o.timeout); err != nil {
return err
}
var res []error
if o.Page != nil {
// query param page
var qrPage int64
if o.Page != nil {
qrPage = *o.Page
}
qPage := swag.FormatInt64(qrPage)
if qPage != "" {
if err := r.SetQueryParam("page", qPage); err != nil {
return err
}
}
}
if o.PageSize != nil {
// query param pageSize
var qrPageSize int64
if o.PageSize != nil {
qrPageSize = *o.PageSize
}
qPageSize := swag.FormatInt64(qrPageSize)
if qPageSize != "" {
if err := r.SetQueryParam("pageSize", qPageSize); err != nil {
return err
}
}
}
if o.Upstream != nil {
// query param upstream
var qrUpstream bool
if o.Upstream != nil {
qrUpstream = *o.Upstream
}
qUpstream := swag.FormatBool(qrUpstream)
if qUpstream != "" {
if err := r.SetQueryParam("upstream", qUpstream); 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 tools
// 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"
)
// AdminGarmAgentListReader is a Reader for the AdminGarmAgentList structure.
type AdminGarmAgentListReader struct {
formats strfmt.Registry
}
// ReadResponse reads a server response into the received o.
func (o *AdminGarmAgentListReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
switch response.Code() {
case 200:
result := NewAdminGarmAgentListOK()
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
return result, nil
case 400:
result := NewAdminGarmAgentListBadRequest()
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
return nil, result
default:
return nil, runtime.NewAPIError("[GET /tools/garm-agent] AdminGarmAgentList", response, response.Code())
}
}
// NewAdminGarmAgentListOK creates a AdminGarmAgentListOK with default headers values
func NewAdminGarmAgentListOK() *AdminGarmAgentListOK {
return &AdminGarmAgentListOK{}
}
/*
AdminGarmAgentListOK describes a response with status code 200, with default header values.
GARMAgentToolsPaginatedResponse
*/
type AdminGarmAgentListOK struct {
Payload garm_params.GARMAgentToolsPaginatedResponse
}
// IsSuccess returns true when this admin garm agent list o k response has a 2xx status code
func (o *AdminGarmAgentListOK) IsSuccess() bool {
return true
}
// IsRedirect returns true when this admin garm agent list o k response has a 3xx status code
func (o *AdminGarmAgentListOK) IsRedirect() bool {
return false
}
// IsClientError returns true when this admin garm agent list o k response has a 4xx status code
func (o *AdminGarmAgentListOK) IsClientError() bool {
return false
}
// IsServerError returns true when this admin garm agent list o k response has a 5xx status code
func (o *AdminGarmAgentListOK) IsServerError() bool {
return false
}
// IsCode returns true when this admin garm agent list o k response a status code equal to that given
func (o *AdminGarmAgentListOK) IsCode(code int) bool {
return code == 200
}
// Code gets the status code for the admin garm agent list o k response
func (o *AdminGarmAgentListOK) Code() int {
return 200
}
func (o *AdminGarmAgentListOK) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[GET /tools/garm-agent][%d] adminGarmAgentListOK %s", 200, payload)
}
func (o *AdminGarmAgentListOK) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[GET /tools/garm-agent][%d] adminGarmAgentListOK %s", 200, payload)
}
func (o *AdminGarmAgentListOK) GetPayload() garm_params.GARMAgentToolsPaginatedResponse {
return o.Payload
}
func (o *AdminGarmAgentListOK) 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
}
// NewAdminGarmAgentListBadRequest creates a AdminGarmAgentListBadRequest with default headers values
func NewAdminGarmAgentListBadRequest() *AdminGarmAgentListBadRequest {
return &AdminGarmAgentListBadRequest{}
}
/*
AdminGarmAgentListBadRequest describes a response with status code 400, with default header values.
APIErrorResponse
*/
type AdminGarmAgentListBadRequest struct {
Payload apiserver_params.APIErrorResponse
}
// IsSuccess returns true when this admin garm agent list bad request response has a 2xx status code
func (o *AdminGarmAgentListBadRequest) IsSuccess() bool {
return false
}
// IsRedirect returns true when this admin garm agent list bad request response has a 3xx status code
func (o *AdminGarmAgentListBadRequest) IsRedirect() bool {
return false
}
// IsClientError returns true when this admin garm agent list bad request response has a 4xx status code
func (o *AdminGarmAgentListBadRequest) IsClientError() bool {
return true
}
// IsServerError returns true when this admin garm agent list bad request response has a 5xx status code
func (o *AdminGarmAgentListBadRequest) IsServerError() bool {
return false
}
// IsCode returns true when this admin garm agent list bad request response a status code equal to that given
func (o *AdminGarmAgentListBadRequest) IsCode(code int) bool {
return code == 400
}
// Code gets the status code for the admin garm agent list bad request response
func (o *AdminGarmAgentListBadRequest) Code() int {
return 400
}
func (o *AdminGarmAgentListBadRequest) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[GET /tools/garm-agent][%d] adminGarmAgentListBadRequest %s", 400, payload)
}
func (o *AdminGarmAgentListBadRequest) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[GET /tools/garm-agent][%d] adminGarmAgentListBadRequest %s", 400, payload)
}
func (o *AdminGarmAgentListBadRequest) GetPayload() apiserver_params.APIErrorResponse {
return o.Payload
}
func (o *AdminGarmAgentListBadRequest) 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

@ -1,198 +0,0 @@
// Code generated by go-swagger; DO NOT EDIT.
package tools
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"net/http"
"time"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
cr "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// NewGarmAgentListParams creates a new GarmAgentListParams 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 NewGarmAgentListParams() *GarmAgentListParams {
return &GarmAgentListParams{
timeout: cr.DefaultTimeout,
}
}
// NewGarmAgentListParamsWithTimeout creates a new GarmAgentListParams object
// with the ability to set a timeout on a request.
func NewGarmAgentListParamsWithTimeout(timeout time.Duration) *GarmAgentListParams {
return &GarmAgentListParams{
timeout: timeout,
}
}
// NewGarmAgentListParamsWithContext creates a new GarmAgentListParams object
// with the ability to set a context for a request.
func NewGarmAgentListParamsWithContext(ctx context.Context) *GarmAgentListParams {
return &GarmAgentListParams{
Context: ctx,
}
}
// NewGarmAgentListParamsWithHTTPClient creates a new GarmAgentListParams object
// with the ability to set a custom HTTPClient for a request.
func NewGarmAgentListParamsWithHTTPClient(client *http.Client) *GarmAgentListParams {
return &GarmAgentListParams{
HTTPClient: client,
}
}
/*
GarmAgentListParams contains all the parameters to send to the API endpoint
for the garm agent list operation.
Typically these are written to a http.Request.
*/
type GarmAgentListParams struct {
/* Page.
The page at which to list.
*/
Page *int64
/* PageSize.
Number of items per page.
*/
PageSize *int64
timeout time.Duration
Context context.Context
HTTPClient *http.Client
}
// WithDefaults hydrates default values in the garm agent list params (not the query body).
//
// All values with no default are reset to their zero value.
func (o *GarmAgentListParams) WithDefaults() *GarmAgentListParams {
o.SetDefaults()
return o
}
// SetDefaults hydrates default values in the garm agent list params (not the query body).
//
// All values with no default are reset to their zero value.
func (o *GarmAgentListParams) SetDefaults() {
// no default values defined for this parameter
}
// WithTimeout adds the timeout to the garm agent list params
func (o *GarmAgentListParams) WithTimeout(timeout time.Duration) *GarmAgentListParams {
o.SetTimeout(timeout)
return o
}
// SetTimeout adds the timeout to the garm agent list params
func (o *GarmAgentListParams) SetTimeout(timeout time.Duration) {
o.timeout = timeout
}
// WithContext adds the context to the garm agent list params
func (o *GarmAgentListParams) WithContext(ctx context.Context) *GarmAgentListParams {
o.SetContext(ctx)
return o
}
// SetContext adds the context to the garm agent list params
func (o *GarmAgentListParams) SetContext(ctx context.Context) {
o.Context = ctx
}
// WithHTTPClient adds the HTTPClient to the garm agent list params
func (o *GarmAgentListParams) WithHTTPClient(client *http.Client) *GarmAgentListParams {
o.SetHTTPClient(client)
return o
}
// SetHTTPClient adds the HTTPClient to the garm agent list params
func (o *GarmAgentListParams) SetHTTPClient(client *http.Client) {
o.HTTPClient = client
}
// WithPage adds the page to the garm agent list params
func (o *GarmAgentListParams) WithPage(page *int64) *GarmAgentListParams {
o.SetPage(page)
return o
}
// SetPage adds the page to the garm agent list params
func (o *GarmAgentListParams) SetPage(page *int64) {
o.Page = page
}
// WithPageSize adds the pageSize to the garm agent list params
func (o *GarmAgentListParams) WithPageSize(pageSize *int64) *GarmAgentListParams {
o.SetPageSize(pageSize)
return o
}
// SetPageSize adds the pageSize to the garm agent list params
func (o *GarmAgentListParams) SetPageSize(pageSize *int64) {
o.PageSize = pageSize
}
// WriteToRequest writes these params to a swagger request
func (o *GarmAgentListParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
if err := r.SetTimeout(o.timeout); err != nil {
return err
}
var res []error
if o.Page != nil {
// query param page
var qrPage int64
if o.Page != nil {
qrPage = *o.Page
}
qPage := swag.FormatInt64(qrPage)
if qPage != "" {
if err := r.SetQueryParam("page", qPage); err != nil {
return err
}
}
}
if o.PageSize != nil {
// query param pageSize
var qrPageSize int64
if o.PageSize != nil {
qrPageSize = *o.PageSize
}
qPageSize := swag.FormatInt64(qrPageSize)
if qPageSize != "" {
if err := r.SetQueryParam("pageSize", qPageSize); err != nil {
return err
}
}
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}

View file

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

@ -56,7 +56,7 @@ type ClientOption func(*runtime.ClientOperation)
// ClientService is the interface for Client methods
type ClientService interface {
GarmAgentList(params *GarmAgentListParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GarmAgentListOK, error)
AdminGarmAgentList(params *AdminGarmAgentListParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*AdminGarmAgentListOK, error)
UploadGARMAgentTool(params *UploadGARMAgentToolParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UploadGARMAgentToolOK, error)
@ -64,22 +64,22 @@ type ClientService interface {
}
/*
GarmAgentList lists g a r m agent tools
AdminGarmAgentList lists g a r m agent tools for admin users
*/
func (a *Client) GarmAgentList(params *GarmAgentListParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GarmAgentListOK, error) {
func (a *Client) AdminGarmAgentList(params *AdminGarmAgentListParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*AdminGarmAgentListOK, error) {
// TODO: Validate the params before sending
if params == nil {
params = NewGarmAgentListParams()
params = NewAdminGarmAgentListParams()
}
op := &runtime.ClientOperation{
ID: "GarmAgentList",
ID: "AdminGarmAgentList",
Method: "GET",
PathPattern: "/tools/garm-agent",
ProducesMediaTypes: []string{"application/json"},
ConsumesMediaTypes: []string{"application/json"},
Schemes: []string{"http"},
Params: params,
Reader: &GarmAgentListReader{formats: a.formats},
Reader: &AdminGarmAgentListReader{formats: a.formats},
AuthInfo: authInfo,
Context: params.Context,
Client: params.HTTPClient,
@ -92,13 +92,13 @@ func (a *Client) GarmAgentList(params *GarmAgentListParams, authInfo runtime.Cli
if err != nil {
return nil, err
}
success, ok := result.(*GarmAgentListOK)
success, ok := result.(*AdminGarmAgentListOK)
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 GarmAgentList: API contract not enforced by server. Client expected to get an error, but got: %T", result)
msg := fmt.Sprintf("unexpected success response for AdminGarmAgentList: API contract not enforced by server. Client expected to get an error, but got: %T", result)
panic(msg)
}

View file

@ -197,25 +197,29 @@ var controllerToolsListCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List GARM agent tools",
Long: `List all GARM agent tools available in the controller.`,
Long: `List all GARM agent tools available in the controller. Use --upstream to list tools from the upstream cached release.`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, _ []string) error {
if needsInit {
return errNeedsInitError
}
showTools := apiClientTools.NewGarmAgentListParams()
showTools := apiClientTools.NewAdminGarmAgentListParams()
if cmd.Flags().Changed("page") {
showTools.Page = &fileObjPage
}
if cmd.Flags().Changed("page-size") {
showTools.PageSize = &fileObjPageSize
}
response, err := apiCli.Tools.GarmAgentList(showTools, authToken)
if cmd.Flags().Changed("upstream") {
upstreamVal := true
showTools.Upstream = &upstreamVal
}
response, err := apiCli.Tools.AdminGarmAgentList(showTools, authToken)
if err != nil {
return err
}
formatGARMToolsList(response.Payload)
formatGARMToolsList(response.Payload, cmd.Flags().Changed("upstream"))
return nil
},
}
@ -521,6 +525,7 @@ func init() {
controllerToolsListCmd.Flags().Int64Var(&fileObjPage, "page", 0, "The tools page to display")
controllerToolsListCmd.Flags().Int64Var(&fileObjPageSize, "page-size", 25, "Total number of results per page")
controllerToolsListCmd.Flags().Bool("upstream", false, "List tools from the upstream cached release instead of the local object store")
controllerToolsUploadCmd.Flags().StringVar(&toolFilePath, "file", "", "Path to the garm-agent binary file (required)")
controllerToolsUploadCmd.Flags().StringVar(&toolOSType, "os", "", "Operating system: linux or windows (required)")
@ -550,17 +555,25 @@ func init() {
rootCmd.AddCommand(controllerCmd)
}
func formatGARMToolsList(files params.GARMAgentToolsPaginatedResponse) {
func formatGARMToolsList(files params.GARMAgentToolsPaginatedResponse, upstream bool) {
if outputFormat == common.OutputFormatJSON {
printAsJSON(files)
return
}
t := table.NewWriter()
// Define column count
numCols := 8
t.Style().Options.SeparateHeader = true
t.Style().Options.SeparateRows = true
var numCols int
var header table.Row
if upstream {
numCols = 6
header = table.Row{"Name", "Size", "Version", "OS Type", "OS Architecture", "Download URL"}
} else {
numCols = 8
header = table.Row{"ID", "Name", "Size", "Version", "OS Type", "OS Architecture", "Created", "Updated"}
}
// Page header - fill all columns with the same text
pageHeaderText := fmt.Sprintf("Page %d of %d", files.CurrentPage, files.Pages)
pageHeader := make(table.Row, numCols)
@ -571,18 +584,23 @@ func formatGARMToolsList(files params.GARMAgentToolsPaginatedResponse) {
AutoMerge: true,
AutoMergeAlign: text.AlignCenter,
})
// Column headers
header := table.Row{"ID", "Name", "Size", "Version", "OS Type", "OS Architecture", "Created", "Updated"}
t.AppendHeader(header)
// Right-align numeric columns
t.SetColumnConfigs([]table.ColumnConfig{
{Number: 1, Align: text.AlignRight},
{Number: 3, Align: text.AlignRight},
})
for _, val := range files.Results {
row := table.Row{val.ID, val.Name, formatSize(val.Size), val.Version, val.OSType, val.OSArch, val.CreatedAt.Format("2006-01-02 15:04:05"), val.UpdatedAt.Format("2006-01-02 15:04:05")}
t.AppendRow(row)
if upstream {
t.SetColumnConfigs([]table.ColumnConfig{
{Number: 2, Align: text.AlignRight},
})
for _, val := range files.Results {
t.AppendRow(table.Row{val.Name, formatSize(val.Size), val.Version, val.OSType, val.OSArch, val.DownloadURL})
}
} else {
t.SetColumnConfigs([]table.ColumnConfig{
{Number: 1, Align: text.AlignRight},
{Number: 3, Align: text.AlignRight},
})
for _, val := range files.Results {
t.AppendRow(table.Row{val.ID, val.Name, formatSize(val.Size), val.Version, val.OSType, val.OSArch, val.CreatedAt, val.UpdatedAt})
}
}
fmt.Println(t.Render())
}

View file

@ -1488,6 +1488,14 @@ type PaginatedResponse[T any] struct {
// swagger:model FileObjectPaginatedResponse
type FileObjectPaginatedResponse = PaginatedResponse[FileObject]
const (
// ToolSourceLocal indicates the tool is stored in the internal object store.
ToolSourceLocal = "local"
// ToolSourceUpstream indicates the tool is only available from the upstream
// cached release and has not been downloaded locally.
ToolSourceUpstream = "upstream"
)
// swagger:model GARMAgentTool
type GARMAgentTool struct {
ID uint `json:"id"`
@ -1507,6 +1515,11 @@ type GARMAgentTool struct {
// from a release URL, this will hold the release URL where the tools
// originated from.
Origin string `json:"origin"`
// Source indicates where this tool is currently stored.
// "local" means the tool is stored in the internal object store.
// "upstream" means the tool is only available from the upstream
// cached release and has not been downloaded locally.
Source string `json:"source"`
}
// swagger:model GARMAgentToolsPaginatedResponse

View file

@ -26,6 +26,7 @@ import (
"log/slog"
"net/url"
"strings"
"time"
"github.com/cloudbase/garm-provider-common/cloudconfig"
"github.com/cloudbase/garm-provider-common/defaults"
@ -173,30 +174,73 @@ func (r *Runner) getRunnerInstallTemplateContext(instance params.Instance, entit
return installRunnerParams, nil
}
// getAgentTool retrieves the GARM agent tool for the given OS type and architecture.
// Priority:
// 1. Tools from object store (manual uploads or synced)
// 2. Tools from cached upstream release (GitHub release API data)
// Returns nil if no tools are available from any source.
func (r *Runner) getAgentTool(ctx context.Context, osType commonParams.OSType, osArch commonParams.OSArch) *params.GARMAgentTool {
// getUpstreamToolsForGaps returns upstream cached tools for os/arch combinations
// not already covered by object store tools. If osType and osArch are non-nil,
// only the matching platform is considered (instance-scoped). If nil, all
// platforms are considered (admin-scoped).
func getUpstreamToolsForGaps(objectStoreTools []params.GARMAgentTool, osType *commonParams.OSType, osArch *commonParams.OSArch) []params.GARMAgentTool {
ctrlInfo := cache.ControllerInfo()
// Check object store first (for both manual uploads and synced tools)
agentTools, err := r.GetGARMTools(ctx, 0, 100)
if err != nil && !errors.Is(err, runnerErrors.ErrNotFound) {
slog.WarnContext(ctx, "failed to query garm agent tools", "error", err)
// Check staleness - don't use cached data older than 24 hours
if ctrlInfo.CachedGARMAgentReleaseFetchedAt == nil {
return nil
}
if time.Since(*ctrlInfo.CachedGARMAgentReleaseFetchedAt) > 24*time.Hour {
return nil
}
if ctrlInfo.CachedGARMAgentTools == nil {
return nil
}
// Filter results to match the requested OS type and architecture
for _, tool := range agentTools.Results {
if tool.OSType == osType && tool.OSArch == osArch {
return &tool
// Build set of os_type/os_arch keys already covered by object store tools
covered := make(map[string]bool, len(objectStoreTools))
for _, tool := range objectStoreTools {
covered[string(tool.OSType)+"/"+string(tool.OSArch)] = true
}
var result []params.GARMAgentTool
for key, tool := range ctrlInfo.CachedGARMAgentTools {
if covered[key] {
continue
}
// For instance-scoped calls, only include matching os/arch
if osType != nil && osArch != nil {
if tool.OSType != *osType || tool.OSArch != *osArch {
continue
}
}
tool.Source = params.ToolSourceUpstream
result = append(result, tool)
}
return result
}
// getInstanceAgentTool retrieves the GARM agent tool for the given instance.
// It first checks the local object store, then falls back to upstream cached tools.
// Returns nil if no tools are available from any source.
func (r *Runner) getInstanceAgentTool(instance params.Instance) *params.GARMAgentTool {
tags := []string{
garmAgentFileTag,
"os_type=" + string(instance.OSType),
"os_arch=" + string(instance.OSArch),
}
files, err := r.store.SearchFileObjectByTags(r.ctx, tags, 0, 1)
if err == nil && files.TotalCount > 0 {
downloadURL, err := url.JoinPath(instance.MetadataURL, "tools/garm-agent", fmt.Sprintf("%d", files.Results[0].ID), "download")
if err == nil {
tool, err := fileObjectToGARMTool(files.Results[0], downloadURL)
if err == nil {
return &tool
}
}
}
// No tools in object store - fall back to cached upstream release data
tool := ctrlInfo.GetCachedAgentTool(string(osType), string(osArch))
return tool
// Fall back to upstream cached tools
upstreamTools := getUpstreamToolsForGaps(nil, &instance.OSType, &instance.OSArch)
if len(upstreamTools) > 0 {
return &upstreamTools[0]
}
return nil
}
func (r *Runner) GetInstanceMetadata(ctx context.Context) (params.InstanceMetadata, error) {
@ -267,7 +311,7 @@ func (r *Runner) GetInstanceMetadata(ctx context.Context) (params.InstanceMetada
}
if dbEntity.AgentMode {
agentTool := r.getAgentTool(ctx, instance.OSType, instance.OSArch)
agentTool := r.getInstanceAgentTool(instance)
// If we have tools, set agent metadata
if agentTool != nil {
@ -588,22 +632,81 @@ func fileObjectToGARMTool(obj params.FileObject, downloadURL string) (params.GAR
DownloadURL: downloadURL,
Version: version,
Origin: origin,
Source: params.ToolSourceLocal,
}
return res, nil
}
func (r *Runner) GetGARMTools(ctx context.Context, page, pageSize uint64) (params.GARMAgentToolsPaginatedResponse, error) {
// GetGARMTools lists GARM agent tools available to admin users.
// When upstream is false, it lists tools from the local object store.
// When upstream is true, it lists tools from the cached upstream release JSON.
func (r *Runner) GetGARMTools(ctx context.Context, page, pageSize uint64, upstream bool) (params.GARMAgentToolsPaginatedResponse, error) {
if !auth.IsAdmin(ctx) {
return params.GARMAgentToolsPaginatedResponse{}, runnerErrors.ErrUnauthorized
}
if upstream {
return r.getUpstreamGARMTools()
}
tags := []string{
garmAgentFileTag,
}
metadataURL := cache.ControllerInfo().MetadataURL
files, err := r.store.SearchFileObjectByTags(r.ctx, tags, page, pageSize)
if err != nil {
return params.GARMAgentToolsPaginatedResponse{}, fmt.Errorf("failed to list files: %w", err)
}
var tools []params.GARMAgentTool
for _, val := range files.Results {
objectIDAsString := fmt.Sprintf("%d", val.ID)
downloadURL, err := url.JoinPath(metadataURL, "tools/garm-agent", objectIDAsString, "download")
if err != nil {
return params.GARMAgentToolsPaginatedResponse{}, fmt.Errorf("failed to construct agent tools download URL: %w", err)
}
res, err := fileObjectToGARMTool(val, downloadURL)
if err != nil {
return params.GARMAgentToolsPaginatedResponse{}, fmt.Errorf("failed parse tools object: %w", err)
}
tools = append(tools, res)
}
return params.GARMAgentToolsPaginatedResponse{
TotalCount: files.TotalCount,
Pages: files.Pages,
CurrentPage: files.CurrentPage,
NextPage: files.NextPage,
PreviousPage: files.PreviousPage,
Results: tools,
}, nil
}
// getUpstreamGARMTools returns tools from the cached upstream release JSON.
func (r *Runner) getUpstreamGARMTools() (params.GARMAgentToolsPaginatedResponse, error) {
upstreamTools := getUpstreamToolsForGaps(nil, nil, nil)
return params.GARMAgentToolsPaginatedResponse{
TotalCount: uint64(len(upstreamTools)),
Pages: 1,
CurrentPage: 0,
Results: upstreamTools,
}, nil
}
// GetAgentGARMTools lists GARM agent tools available for an instance, filtered by
// the instance's OS type and architecture. On the last page, upstream cached tools
// are merged to fill gaps for os/arch combos not present in the local object store.
func (r *Runner) GetAgentGARMTools(ctx context.Context, page, pageSize uint64) (params.GARMAgentToolsPaginatedResponse, error) {
instance, err := validateInstanceState(ctx)
if err != nil {
if !auth.IsAdmin(ctx) {
return params.GARMAgentToolsPaginatedResponse{}, runnerErrors.ErrUnauthorized
}
} else {
tags = append(tags, "os_type="+string(instance.OSType))
tags = append(tags, "os_arch="+string(instance.OSArch))
return params.GARMAgentToolsPaginatedResponse{}, runnerErrors.ErrUnauthorized
}
tags := []string{
garmAgentFileTag,
"os_type=" + string(instance.OSType),
"os_arch=" + string(instance.OSArch),
}
files, err := r.store.SearchFileObjectByTags(r.ctx, tags, page, pageSize)
@ -624,6 +727,14 @@ func (r *Runner) GetGARMTools(ctx context.Context, page, pageSize uint64) (param
}
tools = append(tools, res)
}
// On the last page, merge upstream cached tools that fill gaps
// (os/arch combos not present in the object store).
if files.NextPage == nil {
upstreamTools := getUpstreamToolsForGaps(tools, &instance.OSType, &instance.OSArch)
tools = append(tools, upstreamTools...)
}
return params.GARMAgentToolsPaginatedResponse{
TotalCount: files.TotalCount,
Pages: files.Pages,

View file

@ -23,6 +23,7 @@ import (
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
@ -739,6 +740,11 @@ func (s *MetadataTestSuite) TestGetInstanceMetadataBasicFields() {
}
func (s *MetadataTestSuite) TestGetInstanceMetadataWithAgentModeNoTools() {
// Clear any upstream tools from controller cache (may be set by other tests)
cache.SetControllerCache(params.ControllerInfo{
MetadataURL: "http://metadata.example.com",
})
// Set up runner tools cache
tools := []commonParams.RunnerApplicationDownload{
{
@ -789,6 +795,11 @@ func (s *MetadataTestSuite) TestGetInstanceMetadataAgentModeDisabledByDefault()
}
func (s *MetadataTestSuite) TestGetInstanceMetadataWithAgentModeToolsCountZero() {
// Clear any upstream tools from controller cache (may be set by other tests)
cache.SetControllerCache(params.ControllerInfo{
MetadataURL: "http://metadata.example.com",
})
// Set up runner tools cache
tools := []commonParams.RunnerApplicationDownload{
{
@ -820,6 +831,11 @@ func (s *MetadataTestSuite) TestGetInstanceMetadataWithAgentModeToolsCountZero()
}
func (s *MetadataTestSuite) TestGetInstanceMetadataWithAgentModeGetToolsReturnsNotFoundError() {
// Clear any upstream tools from controller cache (may be set by other tests)
cache.SetControllerCache(params.ControllerInfo{
MetadataURL: "http://metadata.example.com",
})
// Set up runner tools cache
tools := []commonParams.RunnerApplicationDownload{
{
@ -980,27 +996,161 @@ func (s *MetadataTestSuite) TestFileObjectToGARMTool() {
}
func (s *MetadataTestSuite) TestGetGARMTools() {
// GetGARMTools requires file objects in database
// This is tested in file_store_test.go and garm_tools_test.go
// Here we just test the authorization paths
_, err := s.Runner.GetGARMTools(s.instanceCtx, 0, 25)
// GetGARMTools is admin-only, should succeed for admin context
_, err := s.Runner.GetGARMTools(s.adminCtx, 0, 25, false)
s.Require().NoError(err)
}
// Should not error on authorization (might have no results)
func (s *MetadataTestSuite) TestGetAgentGARMTools() {
// GetAgentGARMTools is instance-only, should succeed for instance context
_, err := s.Runner.GetAgentGARMTools(s.instanceCtx, 0, 25)
s.Require().NoError(err)
}
func (s *MetadataTestSuite) TestGetGARMToolsUnauthorized() {
_, err := s.Runner.GetGARMTools(s.unauthorizedCtx, 0, 25)
_, err := s.Runner.GetGARMTools(s.unauthorizedCtx, 0, 25, false)
s.Require().NotNil(err)
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
}
func (s *MetadataTestSuite) TestGetGARMToolsInvalidState() {
_, err := s.Runner.GetGARMTools(s.invalidInstanceCtx, 0, 25)
func (s *MetadataTestSuite) TestGetGARMToolsInstanceUnauthorized() {
// GetGARMTools is admin-only, instance context should fail
_, err := s.Runner.GetGARMTools(s.instanceCtx, 0, 25, false)
s.Require().NotNil(err)
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
}
func (s *MetadataTestSuite) TestGetAgentGARMToolsUnauthorized() {
_, err := s.Runner.GetAgentGARMTools(s.unauthorizedCtx, 0, 25)
s.Require().NotNil(err)
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
}
func (s *MetadataTestSuite) TestGetAgentGARMToolsInvalidState() {
_, err := s.Runner.GetAgentGARMTools(s.invalidInstanceCtx, 0, 25)
s.Require().NotNil(err)
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
}
func (s *MetadataTestSuite) TestGetAgentGARMToolsMergesUpstreamTools() {
// Set up controller cache with upstream tools but no object store tools
now := time.Now()
cache.SetControllerCache(params.ControllerInfo{
MetadataURL: "http://metadata.example.com",
CachedGARMAgentReleaseFetchedAt: &now,
CachedGARMAgentTools: map[string]params.GARMAgentTool{
"linux/amd64": {
Name: "garm-agent-linux-amd64",
Version: "v0.1.0",
OSType: commonParams.Linux,
OSArch: commonParams.Amd64,
DownloadURL: "https://github.com/example/releases/download/v0.1.0/garm-agent-linux-amd64",
},
"linux/arm64": {
Name: "garm-agent-linux-arm64",
Version: "v0.1.0",
OSType: commonParams.Linux,
OSArch: commonParams.Arm64,
DownloadURL: "https://github.com/example/releases/download/v0.1.0/garm-agent-linux-arm64",
},
},
})
// Instance context filters to linux/amd64 - should get the upstream tool
result, err := s.Runner.GetAgentGARMTools(s.instanceCtx, 0, 25)
s.Require().NoError(err)
s.Require().Len(result.Results, 1)
s.Require().Equal("garm-agent-linux-amd64", result.Results[0].Name)
s.Require().Equal(params.ToolSourceUpstream, result.Results[0].Source)
}
func (s *MetadataTestSuite) TestGetAgentGARMToolsObjectStoreOverridesUpstream() {
// Set up controller cache with upstream tools
now := time.Now()
cache.SetControllerCache(params.ControllerInfo{
MetadataURL: "http://metadata.example.com",
CachedGARMAgentReleaseFetchedAt: &now,
CachedGARMAgentTools: map[string]params.GARMAgentTool{
"linux/amd64": {
Name: "garm-agent-linux-amd64",
Version: "v0.1.0",
OSType: commonParams.Linux,
OSArch: commonParams.Amd64,
DownloadURL: "https://github.com/example/releases/download/v0.1.0/garm-agent-linux-amd64",
},
"linux/arm64": {
Name: "garm-agent-linux-arm64",
Version: "v0.1.0",
OSType: commonParams.Linux,
OSArch: commonParams.Arm64,
DownloadURL: "https://github.com/example/releases/download/v0.1.0/garm-agent-linux-arm64",
},
},
})
// Create object store tool for linux/amd64
agentBinary := []byte("fake garm agent binary")
_, err := s.Runner.CreateGARMTool(s.adminCtx, params.CreateGARMToolParams{
Name: "garm-agent-linux-amd64",
Size: int64(len(agentBinary)),
OSType: commonParams.Linux,
OSArch: commonParams.Amd64,
Version: "v1.0.0",
}, bytes.NewReader(agentBinary))
s.Require().NoError(err)
// Instance is linux/amd64 - should get local tool, not upstream
result, err := s.Runner.GetAgentGARMTools(s.instanceCtx, 0, 25)
s.Require().NoError(err)
s.Require().Len(result.Results, 1)
s.Require().Equal(params.ToolSourceLocal, result.Results[0].Source)
s.Require().Equal("v1.0.0", result.Results[0].Version)
}
func (s *MetadataTestSuite) TestGetAgentGARMToolsStaleUpstreamNotIncluded() {
// Set up controller cache with stale upstream tools (>24h old)
staleTime := time.Now().Add(-25 * time.Hour)
cache.SetControllerCache(params.ControllerInfo{
MetadataURL: "http://metadata.example.com",
CachedGARMAgentReleaseFetchedAt: &staleTime,
CachedGARMAgentTools: map[string]params.GARMAgentTool{
"linux/amd64": {
Name: "garm-agent-linux-amd64",
Version: "v0.1.0",
OSType: commonParams.Linux,
OSArch: commonParams.Amd64,
},
},
})
// Instance context - stale upstream tools should not be included
result, err := s.Runner.GetAgentGARMTools(s.instanceCtx, 0, 25)
s.Require().NoError(err)
s.Require().Empty(result.Results)
}
func (s *MetadataTestSuite) TestGetGARMToolsAdminNoUpstream() {
// Set up controller cache with upstream tools but no object store tools
now := time.Now()
cache.SetControllerCache(params.ControllerInfo{
MetadataURL: "http://metadata.example.com",
CachedGARMAgentReleaseFetchedAt: &now,
CachedGARMAgentTools: map[string]params.GARMAgentTool{
"linux/amd64": {
Name: "garm-agent-linux-amd64",
Version: "v0.1.0",
OSType: commonParams.Linux,
OSArch: commonParams.Amd64,
},
},
})
// Admin context should NOT include upstream tools
result, err := s.Runner.GetGARMTools(s.adminCtx, 0, 25, false)
s.Require().NoError(err)
s.Require().Empty(result.Results)
}
func (s *MetadataTestSuite) TestShowGARMToolsUnauthorized() {
_, err := s.Runner.ShowGARMTools(s.unauthorizedCtx, 1)
s.Require().NotNil(err)