garm/cmd/garm-cli/cmd/util.go
Gabriel Adrian Samfira 6c46cf9be1 Add API, CLI and web UI integration for objects
This change adds the API endpoints, the CLI commands and the web UI elements
needed to manage objects in GARMs internal storage.

This storage system is meant to be used to distribute the garm-agent and as a
single source of truth for provider binaries, when we will add the ability for GARM
to scale out.

Potentially, we can also use this in air gapped systems to distribute the runner binaries
for forges that don't have their own internal storage system (like GHES).

Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
2025-10-08 22:22:58 +03:00

176 lines
4.6 KiB
Go

package cmd
import (
"fmt"
"io"
"math"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/google/uuid"
apiClientEnterprises "github.com/cloudbase/garm/client/enterprises"
apiClientOrgs "github.com/cloudbase/garm/client/organizations"
apiClientRepos "github.com/cloudbase/garm/client/repositories"
apiTemplates "github.com/cloudbase/garm/client/templates"
"github.com/cloudbase/garm/params"
)
func resolveTemplateAsUint(nameOrID string) (uint, error) {
if parsed, err := strconv.ParseUint(nameOrID, 10, 64); err == nil {
if parsed > math.MaxUint {
return 0, fmt.Errorf("ID is too large")
}
return uint(parsed), nil
}
listTplReq := apiTemplates.NewListTemplatesParams()
listTplReq.PartialName = &nameOrID
response, err := apiCli.Templates.ListTemplates(listTplReq, authToken)
if err != nil {
return 0, fmt.Errorf("failed to list templates")
}
if len(response.Payload) == 0 {
return 0, fmt.Errorf("no such template: %s", nameOrID)
}
exactMatches := []params.Template{}
for _, val := range response.Payload {
if val.Name == nameOrID {
exactMatches = append(exactMatches, val)
}
}
if len(exactMatches) > 1 {
return 0, fmt.Errorf("multiple templates found with name: %s", nameOrID)
}
return exactMatches[0].ID, nil
}
func resolveTemplate(nameOrID string) (float64, error) {
id, err := resolveTemplateAsUint(nameOrID)
if err != nil {
return 0, err
}
return float64(id), nil
}
func resolveRepository(nameOrID, endpoint string) (string, error) {
if nameOrID == "" {
return "", fmt.Errorf("missing repository name or ID")
}
entityID, err := uuid.Parse(nameOrID)
if err == nil {
return entityID.String(), nil
}
parts := strings.SplitN(nameOrID, "/", 2)
if len(parts) < 2 {
// format of friendly name is invalid for a repository.
// Return the string as is.
return nameOrID, nil
}
listReposReq := apiClientRepos.NewListReposParams()
listReposReq.Owner = &parts[0]
listReposReq.Name = &parts[1]
if endpoint != "" {
listReposReq.Endpoint = &endpoint
}
response, err := apiCli.Repositories.ListRepos(listReposReq, authToken)
if err != nil {
return "", err
}
if len(response.Payload) == 0 {
return "", fmt.Errorf("repository %s was not found", nameOrID)
}
if len(response.Payload) > 1 {
return "", fmt.Errorf("multiple repositories with the name %s exist, please use the repository ID or specify the --endpoint parameter", nameOrID)
}
return response.Payload[0].ID, nil
}
func resolveOrganization(nameOrID, endpoint string) (string, error) {
if nameOrID == "" {
return "", fmt.Errorf("missing organization name or ID")
}
entityID, err := uuid.Parse(nameOrID)
if err == nil {
return entityID.String(), nil
}
listOrgsReq := apiClientOrgs.NewListOrgsParams()
listOrgsReq.Name = &nameOrID
if endpoint != "" {
listOrgsReq.Endpoint = &endpoint
}
response, err := apiCli.Organizations.ListOrgs(listOrgsReq, authToken)
if err != nil {
return "", err
}
if len(response.Payload) == 0 {
return "", fmt.Errorf("organization %s was not found", nameOrID)
}
if len(response.Payload) > 1 {
return "", fmt.Errorf("multiple organizations with the name %s exist, please use the organization ID or specify the --endpoint parameter", nameOrID)
}
return response.Payload[0].ID, nil
}
func resolveEnterprise(nameOrID, endpoint string) (string, error) {
if nameOrID == "" {
return "", fmt.Errorf("missing enterprise name or ID")
}
entityID, err := uuid.Parse(nameOrID)
if err == nil {
return entityID.String(), nil
}
listEnterprisesReq := apiClientEnterprises.NewListEnterprisesParams()
listEnterprisesReq.Name = &enterpriseName
if endpoint != "" {
listEnterprisesReq.Endpoint = &endpoint
}
response, err := apiCli.Enterprises.ListEnterprises(listEnterprisesReq, authToken)
if err != nil {
return "", err
}
if len(response.Payload) == 0 {
return "", fmt.Errorf("enterprise %s was not found", nameOrID)
}
if len(response.Payload) > 1 {
return "", fmt.Errorf("multiple enterprises with the name %s exist, please use the enterprise ID or specify the --endpoint parameter", nameOrID)
}
return response.Payload[0].ID, nil
}
type rawClient struct {
BaseURL string
Token string
HTTPClient *http.Client
}
// NewRequest creates a new HTTP request with auth and proper URL
func (c *rawClient) NewRequest(method, path string, body io.Reader) (*http.Request, error) {
url, err := url.JoinPath(c.BaseURL, path)
if err != nil {
return nil, fmt.Errorf("failed to join URL: %w", err)
}
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
// Add JWT token
req.Header.Set("Authorization", "Bearer "+c.Token)
return req, nil
}