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>
176 lines
4.6 KiB
Go
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
|
|
}
|