Add instance metadata URL
The runner metatada URL is meant to give runner install scripts an
easier way to get instance specific metadata, needed for the setup
process. We can use this URL to easier expand installation metadata as
opposed to having to change the cloud config InstallRunnerParams{}.
Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
This commit is contained in:
parent
78856b56b2
commit
b47caa4bf5
4 changed files with 145 additions and 12 deletions
|
|
@ -26,6 +26,21 @@ import (
|
|||
"github.com/cloudbase/garm/apiserver/params"
|
||||
)
|
||||
|
||||
func (a *APIController) InstanceMetadataHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
metadata, err := a.r.GetInstanceMetadata(ctx)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "failed to get instance metadata", "error", err)
|
||||
handleError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(metadata); err != nil {
|
||||
slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *APIController) InstanceGARMToolsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
|
|
|
|||
|
|
@ -159,6 +159,9 @@ func NewAPIRouter(han *controllers.APIController, authMiddleware, initMiddleware
|
|||
metadataRouter := apiSubRouter.PathPrefix("/metadata").Subrouter()
|
||||
metadataRouter.Use(instanceMiddleware.Middleware)
|
||||
|
||||
// Instance metadata
|
||||
metadataRouter.Handle("/runner-metadata/", http.HandlerFunc(han.InstanceMetadataHandler)).Methods("GET", "OPTIONS")
|
||||
metadataRouter.Handle("/runner-metadata", http.HandlerFunc(han.InstanceMetadataHandler)).Methods("GET", "OPTIONS")
|
||||
// Registration token
|
||||
metadataRouter.Handle("/runner-registration-token/", http.HandlerFunc(han.InstanceGithubRegistrationTokenHandler)).Methods("GET", "OPTIONS")
|
||||
metadataRouter.Handle("/runner-registration-token", http.HandlerFunc(han.InstanceGithubRegistrationTokenHandler)).Methods("GET", "OPTIONS")
|
||||
|
|
@ -241,10 +244,11 @@ func NewAPIRouter(han *controllers.APIController, authMiddleware, initMiddleware
|
|||
apiRouter.Handle("/objects/{objectID}/", http.HandlerFunc(han.UpdateFileObject)).Methods("PUT", "OPTIONS")
|
||||
apiRouter.Handle("/objects/{objectID}", http.HandlerFunc(han.UpdateFileObject)).Methods("PUT", "OPTIONS")
|
||||
|
||||
// DELETEME //
|
||||
// Tools
|
||||
apiRouter.Handle("/tools/garm/", http.HandlerFunc(han.InstanceGARMToolsHandler)).Methods("GET", "OPTIONS")
|
||||
apiRouter.Handle("/tools/garm", http.HandlerFunc(han.InstanceGARMToolsHandler)).Methods("GET", "OPTIONS")
|
||||
///////////////////////////////////////////////////////
|
||||
// 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")
|
||||
|
||||
//////////
|
||||
// Jobs //
|
||||
|
|
|
|||
|
|
@ -1350,3 +1350,30 @@ type GARMAgentTool struct {
|
|||
|
||||
// swagger:model GARMAgentToolsPaginatedResponse
|
||||
type GARMAgentToolsPaginatedResponse = PaginatedResponse[GARMAgentTool]
|
||||
|
||||
// swagger:model MetadataServiceAccessDetails
|
||||
type MetadataServiceAccessDetails struct {
|
||||
CallbackURL string `json:"callback_url"`
|
||||
MetadataURL string `json:"metadata_url"`
|
||||
}
|
||||
|
||||
// swagger:model InstanceMetadata
|
||||
type InstanceMetadata struct {
|
||||
MetadataAccess MetadataServiceAccessDetails `json:"metadata_access"`
|
||||
ForgeType EndpointType `json:"forge_type"`
|
||||
// RunnerRegistrationURL is the URL the runner needs to configure itself
|
||||
// against. This can be a repository, organization, enterprise (github)
|
||||
// or system (gitea)
|
||||
RunnerRegistrationURL string `json:"runner_registration_url"`
|
||||
RunnerName string `json:"runner_name"`
|
||||
RunnerLabels []string `json:"runner_labels,omitempty"`
|
||||
CABundle map[string][]byte `json:"ca_bundles,omitempty"`
|
||||
// ExtraSpecs represents the extra specs set on the pool or scale set. No secrets should
|
||||
// be set in extra specs.
|
||||
// Also, the instance metadata should never be saved to disk, and the metadata URL is only
|
||||
// accessible during setup of the runner. The API returns unauthorized once the runner
|
||||
// transitions to failed/idle.
|
||||
ExtraSpecs map[string]any `json:"extra_specs,omitempty"`
|
||||
JITEnabled bool `json:"jit_enabled"`
|
||||
RunnerTools commonParams.RunnerApplicationDownload `json:"runner_tools"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,6 +171,93 @@ func (r *Runner) getRunnerInstallTemplateContext(instance params.Instance, entit
|
|||
return installRunnerParams, nil
|
||||
}
|
||||
|
||||
func (r *Runner) GetInstanceMetadata(ctx context.Context) (params.InstanceMetadata, error) {
|
||||
instance, err := validateInstanceState(ctx)
|
||||
if err != nil {
|
||||
return params.InstanceMetadata{}, runnerErrors.ErrUnauthorized
|
||||
}
|
||||
|
||||
var entityGetter params.EntityGetter
|
||||
var extraSpecs json.RawMessage
|
||||
switch {
|
||||
case instance.PoolID != "":
|
||||
pool, err := r.store.GetPoolByID(r.ctx, instance.PoolID)
|
||||
if err != nil {
|
||||
return params.InstanceMetadata{}, fmt.Errorf("failed to get pool: %w", err)
|
||||
}
|
||||
entityGetter = pool
|
||||
extraSpecs = pool.ExtraSpecs
|
||||
case instance.ScaleSetID != 0:
|
||||
scaleSet, err := r.store.GetScaleSetByID(r.ctx, instance.ScaleSetID)
|
||||
if err != nil {
|
||||
return params.InstanceMetadata{}, fmt.Errorf("failed to get scale set: %w", err)
|
||||
}
|
||||
entityGetter = scaleSet
|
||||
extraSpecs = scaleSet.ExtraSpecs
|
||||
default:
|
||||
// This is not actually an unauthorized scenario. This case means that an
|
||||
// instance was created but it does not belong to any pool or scale set.
|
||||
// This is an internal error state, but it's not something we should expose
|
||||
// to a potential runner that is trying to start.
|
||||
slog.ErrorContext(ctx, "runner is authentic but does not belong to a pool or scale set", "instance_name", instance.Name, "instance_id", instance.ID)
|
||||
return params.InstanceMetadata{}, runnerErrors.ErrUnauthorized
|
||||
}
|
||||
|
||||
entity, err := entityGetter.GetEntity()
|
||||
if err != nil {
|
||||
return params.InstanceMetadata{}, fmt.Errorf("failed to get entity: %w", err)
|
||||
}
|
||||
|
||||
dbEntity, err := r.store.GetForgeEntity(ctx, entity.EntityType, entity.ID)
|
||||
if err != nil {
|
||||
return params.InstanceMetadata{}, fmt.Errorf("failed to get entity: %w", err)
|
||||
}
|
||||
|
||||
ret := params.InstanceMetadata{
|
||||
RunnerName: instance.Name,
|
||||
RunnerLabels: getLabelsForInstance(instance),
|
||||
RunnerRegistrationURL: dbEntity.ForgeURL(),
|
||||
MetadataAccess: params.MetadataServiceAccessDetails{
|
||||
CallbackURL: instance.CallbackURL,
|
||||
MetadataURL: instance.MetadataURL,
|
||||
},
|
||||
ForgeType: dbEntity.Credentials.ForgeType,
|
||||
JITEnabled: len(instance.JitConfiguration) > 0,
|
||||
}
|
||||
|
||||
if len(dbEntity.Credentials.Endpoint.CACertBundle) > 0 {
|
||||
// We can add other CA bundles here as needed.
|
||||
ret.CABundle["forge_ca"] = dbEntity.Credentials.Endpoint.CACertBundle
|
||||
}
|
||||
|
||||
if len(extraSpecs) > 0 {
|
||||
var specs map[string]any
|
||||
if err := json.Unmarshal(extraSpecs, &specs); err != nil {
|
||||
return params.InstanceMetadata{}, fmt.Errorf("failed to decode extra specs: %w", err)
|
||||
}
|
||||
ret.ExtraSpecs = specs
|
||||
}
|
||||
|
||||
tools, err := cache.GetGithubToolsCache(dbEntity.ID)
|
||||
if err != nil {
|
||||
return params.InstanceMetadata{}, fmt.Errorf("failed to find tools: %w", err)
|
||||
}
|
||||
|
||||
filtered, err := util.GetTools(instance.OSType, instance.OSArch, tools)
|
||||
if err != nil {
|
||||
return params.InstanceMetadata{}, fmt.Errorf("no tools available: %w", err)
|
||||
}
|
||||
ret.RunnerTools = filtered
|
||||
|
||||
switch dbEntity.Credentials.ForgeType {
|
||||
case params.GiteaEndpointType:
|
||||
case params.GithubEndpointType:
|
||||
default:
|
||||
return params.InstanceMetadata{}, fmt.Errorf("invalid forge type: %s", dbEntity.Credentials.ForgeType)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *Runner) GetRunnerInstallScript(ctx context.Context) ([]byte, error) {
|
||||
instance, err := validateInstanceState(ctx)
|
||||
if err != nil {
|
||||
|
|
@ -331,7 +418,7 @@ func (r *Runner) GetJITConfigFile(ctx context.Context, file string) ([]byte, err
|
|||
|
||||
func (r *Runner) GetInstanceGithubRegistrationToken(ctx context.Context) (string, error) {
|
||||
// Check if this instance already fetched a registration token or if it was configured using
|
||||
// the new Just In Time runner feature. If we're still using the old way of configuring a runner,
|
||||
// the Just In Time runner feature. If we're still using the old way of configuring a runner,
|
||||
// we only allow an instance to fetch one token. If the instance fails to bootstrap after a token
|
||||
// is fetched, we reset the token fetched field when re-queueing the instance.
|
||||
if auth.InstanceTokenFetched(ctx) || auth.InstanceHasJITConfig(ctx) {
|
||||
|
|
@ -407,7 +494,7 @@ func (r *Runner) GetGARMTools(ctx context.Context, page, pageSize uint64) (param
|
|||
tags := []string{
|
||||
"category=garm-agent",
|
||||
}
|
||||
instance, err := auth.InstanceParams(ctx)
|
||||
instance, err := validateInstanceState(ctx)
|
||||
if err != nil {
|
||||
if !auth.IsAdmin(ctx) {
|
||||
return params.GARMAgentToolsPaginatedResponse{}, runnerErrors.ErrUnauthorized
|
||||
|
|
@ -426,17 +513,17 @@ func (r *Runner) GetGARMTools(ctx context.Context, page, pageSize uint64) (param
|
|||
for _, val := range files.Results {
|
||||
tags := val.Tags
|
||||
var version string
|
||||
var os_type string
|
||||
var os_arch string
|
||||
var osType string
|
||||
var osArch string
|
||||
for _, val := range tags {
|
||||
if strings.HasPrefix(val, "version=") {
|
||||
version = val[8:]
|
||||
}
|
||||
if strings.HasPrefix(val, "os_arch=") {
|
||||
os_arch = val[8:]
|
||||
osArch = val[8:]
|
||||
}
|
||||
if strings.HasPrefix(val, "os_type=") {
|
||||
os_type = val[8:]
|
||||
osType = val[8:]
|
||||
}
|
||||
}
|
||||
res := params.GARMAgentTool{
|
||||
|
|
@ -448,8 +535,8 @@ func (r *Runner) GetGARMTools(ctx context.Context, page, pageSize uint64) (param
|
|||
CreatedAt: val.CreatedAt,
|
||||
UpdatedAt: val.UpdatedAt,
|
||||
FileType: val.FileType,
|
||||
OSType: commonParams.OSType(os_type),
|
||||
OSArch: commonParams.OSArch(os_arch),
|
||||
OSType: commonParams.OSType(osType),
|
||||
OSArch: commonParams.OSArch(osArch),
|
||||
}
|
||||
if version != "" {
|
||||
res.Version = version
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue