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>
378 lines
11 KiB
Go
378 lines
11 KiB
Go
// 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 controllers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gorilla/mux"
|
|
|
|
gErrors "github.com/cloudbase/garm-provider-common/errors"
|
|
commonParams "github.com/cloudbase/garm-provider-common/params"
|
|
"github.com/cloudbase/garm/apiserver/params"
|
|
runnerParams "github.com/cloudbase/garm/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")
|
|
}
|
|
}
|
|
|
|
// swagger:route GET /tools/garm-agent tools GarmAgentList
|
|
//
|
|
// List GARM agent tools.
|
|
//
|
|
// 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
|
|
//
|
|
// Responses:
|
|
// 200: GARMAgentToolsPaginatedResponse
|
|
// 400: APIErrorResponse
|
|
func (a *APIController) InstanceGARMToolsHandler(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
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
func (a *APIController) InstanceShowGARMToolHandler(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
vars := mux.Vars(r)
|
|
objectID, err := getObjectIDFromVars(vars)
|
|
if err != nil {
|
|
slog.ErrorContext(ctx, "failed to get object ID", "error", err)
|
|
handleError(ctx, w, gErrors.NewBadRequestError("invalid objectID: %s", err))
|
|
return
|
|
}
|
|
tools, err := a.r.ShowGARMTools(ctx, objectID)
|
|
if err != nil {
|
|
slog.ErrorContext(ctx, "failed to get garm tools", "error", err)
|
|
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")
|
|
}
|
|
}
|
|
|
|
func (a *APIController) InstanceGARMToolDownloadHandler(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
vars := mux.Vars(r)
|
|
objectID, err := getObjectIDFromVars(vars)
|
|
if err != nil {
|
|
slog.ErrorContext(ctx, "failed to get object ID", "error", err)
|
|
handleError(ctx, w, gErrors.NewBadRequestError("invalid objectID: %s", err))
|
|
return
|
|
}
|
|
|
|
reader, err := a.r.GetGARMToolsReadHandler(ctx, objectID)
|
|
if err != nil {
|
|
handleError(ctx, w, err)
|
|
return
|
|
}
|
|
defer reader.Close()
|
|
if _, err := io.Copy(w, reader); err != nil {
|
|
slog.ErrorContext(ctx, "failed to stream data", "error", err)
|
|
}
|
|
}
|
|
|
|
func (a *APIController) InstanceGithubRegistrationTokenHandler(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
token, err := a.r.GetInstanceGithubRegistrationToken(ctx)
|
|
if err != nil {
|
|
handleError(ctx, w, err)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
if _, err := w.Write([]byte(token)); err != nil {
|
|
slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response")
|
|
}
|
|
}
|
|
|
|
func (a *APIController) JITCredentialsFileHandler(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
vars := mux.Vars(r)
|
|
fileName, ok := vars["fileName"]
|
|
if !ok {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
if err := json.NewEncoder(w).Encode(params.APIErrorResponse{
|
|
Error: "Not Found",
|
|
Details: "Not Found",
|
|
}); err != nil {
|
|
slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response")
|
|
}
|
|
return
|
|
}
|
|
|
|
dotFileName := fmt.Sprintf(".%s", fileName)
|
|
|
|
data, err := a.r.GetJITConfigFile(ctx, dotFileName)
|
|
if err != nil {
|
|
slog.With(slog.Any("error", err)).ErrorContext(ctx, "getting JIT config file")
|
|
handleError(ctx, w, err)
|
|
return
|
|
}
|
|
|
|
// Note the leading dot in the filename
|
|
name := fmt.Sprintf("attachment; filename=%s", dotFileName)
|
|
w.Header().Set("Content-Disposition", name)
|
|
w.Header().Set("Content-Type", "octet-stream")
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(data)))
|
|
w.WriteHeader(http.StatusOK)
|
|
if _, err := w.Write(data); err != nil {
|
|
slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response")
|
|
}
|
|
}
|
|
|
|
func (a *APIController) SystemdServiceNameHandler(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
serviceName, err := a.r.GetRunnerServiceName(ctx)
|
|
if err != nil {
|
|
handleError(ctx, w, err)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusOK)
|
|
if _, err := w.Write([]byte(serviceName)); err != nil {
|
|
slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response")
|
|
}
|
|
}
|
|
|
|
func (a *APIController) SystemdUnitFileHandler(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
runAsUser := r.URL.Query().Get("runAsUser")
|
|
|
|
data, err := a.r.GenerateSystemdUnitFile(ctx, runAsUser)
|
|
if err != nil {
|
|
handleError(ctx, w, err)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusOK)
|
|
if _, err := w.Write(data); err != nil {
|
|
slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response")
|
|
}
|
|
}
|
|
|
|
func (a *APIController) RootCertificateBundleHandler(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
bundle, err := a.r.GetRootCertificateBundle(ctx)
|
|
if err != nil {
|
|
handleError(ctx, w, err)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(bundle); err != nil {
|
|
slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response")
|
|
}
|
|
}
|
|
|
|
// swagger:route POST /tools/garm-agent tools UploadGARMAgentTool
|
|
//
|
|
// Upload a GARM agent tool binary.
|
|
//
|
|
// Uploads a GARM agent tool for a specific OS and architecture.
|
|
// This will automatically replace any existing tool for the same OS/architecture combination.
|
|
//
|
|
// Uses custom headers for metadata:
|
|
//
|
|
// - X-Tool-Name: Name of the tool
|
|
//
|
|
// - X-Tool-Description: Description
|
|
//
|
|
// - X-Tool-OS-Type: OS type (linux or windows)
|
|
//
|
|
// - X-Tool-OS-Arch: Architecture (amd64 or arm64)
|
|
//
|
|
// - X-Tool-Version: Version string
|
|
//
|
|
// Responses:
|
|
// 200: FileObject
|
|
// 400: APIErrorResponse
|
|
// 401: APIErrorResponse
|
|
func (a *APIController) UploadGARMAgentToolHandler(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
// Get metadata from headers
|
|
toolName := r.Header.Get("X-Tool-Name")
|
|
toolDesc := r.Header.Get("X-Tool-Description")
|
|
toolOSType := r.Header.Get("X-Tool-OS-Type")
|
|
toolOSArch := r.Header.Get("X-Tool-OS-Arch")
|
|
toolVersion := r.Header.Get("X-Tool-Version")
|
|
|
|
if toolName == "" || toolOSType == "" || toolOSArch == "" || toolVersion == "" {
|
|
handleError(ctx, w, gErrors.NewBadRequestError("missing required headers: X-Tool-Name, X-Tool-OS-Type, X-Tool-OS-Arch, X-Tool-Version"))
|
|
return
|
|
}
|
|
|
|
// Build params
|
|
createParams := runnerParams.CreateGARMToolParams{
|
|
Name: toolName,
|
|
Description: toolDesc,
|
|
Size: r.ContentLength,
|
|
OSType: commonParams.OSType(toolOSType),
|
|
OSArch: commonParams.OSArch(toolOSArch),
|
|
Version: toolVersion,
|
|
Origin: "manual",
|
|
}
|
|
|
|
// Create the tool (this will handle cleanup of old versions)
|
|
result, err := a.r.CreateGARMTool(ctx, createParams, r.Body)
|
|
if err != nil {
|
|
handleError(ctx, w, err)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(result); err != nil {
|
|
slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response")
|
|
}
|
|
}
|
|
|
|
func (a *APIController) RunnerInstallScriptHandler(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
installScript, err := a.r.GetRunnerInstallScript(ctx)
|
|
if err != nil {
|
|
slog.InfoContext(ctx, "failed to get runner install template", "error", err)
|
|
handleError(ctx, w, err)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusOK)
|
|
if _, err := w.Write(installScript); err != nil {
|
|
slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response")
|
|
}
|
|
}
|