Runners now send status messages

This commit is contained in:
Gabriel Adrian Samfira 2022-05-03 19:49:14 +00:00
parent 6bdb8cd78b
commit 2bd128af13
22 changed files with 741 additions and 105 deletions

View file

@ -449,3 +449,96 @@ func (a *APIController) UpdateRepoPoolHandler(w http.ResponseWriter, r *http.Req
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(pool)
}
func (a *APIController) ListRepoInstancesHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
repoID, ok := vars["repoID"]
if !ok {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(params.APIErrorResponse{
Error: "Bad Request",
Details: "No repo ID specified",
})
return
}
instances, err := a.r.ListRepoInstances(ctx, repoID)
if err != nil {
log.Printf("listing pools: %+v", err)
handleError(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(instances)
}
func (a *APIController) ListPoolInstancesHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
poolID, ok := vars["poolID"]
if !ok {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(params.APIErrorResponse{
Error: "Bad Request",
Details: "No repo ID specified",
})
return
}
instances, err := a.r.ListPoolInstances(ctx, poolID)
if err != nil {
log.Printf("listing pools: %+v", err)
handleError(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(instances)
}
func (a *APIController) GetInstanceHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
instanceName, ok := vars["instanceName"]
if !ok {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(params.APIErrorResponse{
Error: "Bad Request",
Details: "No repo ID specified",
})
return
}
instance, err := a.r.GetInstance(ctx, instanceName)
if err != nil {
log.Printf("listing pools: %+v", err)
handleError(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(instance)
}
func (a *APIController) InstanceStatusMessageHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var updateMessage runnerParams.InstanceUpdateMessage
if err := json.NewDecoder(r.Body).Decode(&updateMessage); err != nil {
log.Printf("failed to decode: %+v", err)
handleError(w, gErrors.ErrBadRequest)
return
}
log.Printf("Update body is: %v", updateMessage)
if err := a.r.AddInstanceStatusMessage(ctx, updateMessage); err != nil {
log.Printf("error saving status message: %+v", err)
handleError(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}

View file

@ -12,7 +12,7 @@ import (
"runner-manager/auth"
)
func NewAPIRouter(han *controllers.APIController, logWriter io.Writer, authMiddleware, initMiddleware auth.Middleware) *mux.Router {
func NewAPIRouter(han *controllers.APIController, logWriter io.Writer, authMiddleware, initMiddleware, instanceMiddleware auth.Middleware) *mux.Router {
router := mux.NewRouter()
log := gorillaHandlers.CombinedLoggingHandler
@ -28,6 +28,11 @@ func NewAPIRouter(han *controllers.APIController, logWriter io.Writer, authMiddl
firstRunRouter := apiSubRouter.PathPrefix("/first-run").Subrouter()
firstRunRouter.Handle("/", log(os.Stdout, http.HandlerFunc(han.FirstRunHandler))).Methods("POST", "OPTIONS")
// Instance callback
callbackRouter := apiSubRouter.PathPrefix("/callbacks").Subrouter()
callbackRouter.Handle("/status/", log(os.Stdout, http.HandlerFunc(han.InstanceStatusMessageHandler))).Methods("POST", "OPTIONS")
callbackRouter.Handle("/status", log(os.Stdout, http.HandlerFunc(han.InstanceStatusMessageHandler))).Methods("POST", "OPTIONS")
callbackRouter.Use(instanceMiddleware.Middleware)
// Login
authRouter := apiSubRouter.PathPrefix("/auth").Subrouter()
authRouter.Handle("/{login:login\\/?}", log(os.Stdout, http.HandlerFunc(han.LoginHandler))).Methods("POST", "OPTIONS")
@ -37,6 +42,17 @@ func NewAPIRouter(han *controllers.APIController, logWriter io.Writer, authMiddl
apiRouter.Use(initMiddleware.Middleware)
apiRouter.Use(authMiddleware.Middleware)
// Runners (instances)
// List pool instances
apiRouter.Handle("/pools/instances/{poolID}/", log(os.Stdout, http.HandlerFunc(han.ListPoolInstancesHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/pools/instances/{poolID}", log(os.Stdout, http.HandlerFunc(han.ListPoolInstancesHandler))).Methods("GET", "OPTIONS")
// Get instance
apiRouter.Handle("/instances/{instanceName}/", log(os.Stdout, http.HandlerFunc(han.GetInstanceHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/instances/{instanceName}", log(os.Stdout, http.HandlerFunc(han.GetInstanceHandler))).Methods("GET", "OPTIONS")
// Delete instance
// apiRouter.Handle("/instances/{instanceName}/", log(os.Stdout, http.HandlerFunc(han.CatchAll))).Methods("DELETE", "OPTIONS")
// apiRouter.Handle("/instances/{instanceName}", log(os.Stdout, http.HandlerFunc(han.CatchAll))).Methods("DELETE", "OPTIONS")
/////////////////////
// Repos and pools //
/////////////////////
@ -56,6 +72,10 @@ func NewAPIRouter(han *controllers.APIController, logWriter io.Writer, authMiddl
apiRouter.Handle("/repositories/{repoID}/pools/", log(os.Stdout, http.HandlerFunc(han.CreateRepoPoolHandler))).Methods("POST", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/pools", log(os.Stdout, http.HandlerFunc(han.CreateRepoPoolHandler))).Methods("POST", "OPTIONS")
// Repo instances list
apiRouter.Handle("/repositories/{repoID}/instances/", log(os.Stdout, http.HandlerFunc(han.ListRepoInstancesHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/instances", log(os.Stdout, http.HandlerFunc(han.ListRepoInstancesHandler))).Methods("GET", "OPTIONS")
// Get repo
apiRouter.Handle("/repositories/{repoID}/", log(os.Stdout, http.HandlerFunc(han.GetRepoByIDHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}", log(os.Stdout, http.HandlerFunc(han.GetRepoByIDHandler))).Methods("GET", "OPTIONS")

View file

@ -8,6 +8,21 @@ import (
type contextFlags string
/*
// InstanceJWTClaims holds JWT claims
type InstanceJWTClaims struct {
ID string `json:"id"`
Name string `json:"name"`
PoolID string `json:"provider_id"`
// Scope is either repository or organization
Scope common.PoolType `json:"scope"`
// Entity is the repo or org name
Entity string `json:"entity"`
jwt.StandardClaims
}
*/
const (
isAdminKey contextFlags = "is_admin"
fullNameKey contextFlags = "full_name"
@ -15,8 +30,81 @@ const (
UserIDFlag contextFlags = "user_id"
isEnabledFlag contextFlags = "is_enabled"
jwtTokenFlag contextFlags = "jwt_token"
instanceIDKey contextFlags = "id"
instanceNameKey contextFlags = "name"
instancePoolIDKey contextFlags = "pool_id"
instancePoolTypeKey contextFlags = "scope"
instanceEntityKey contextFlags = "entity"
)
func SetInstanceID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, instanceIDKey, id)
}
func InstanceID(ctx context.Context) string {
elem := ctx.Value(instanceIDKey)
if elem == nil {
return ""
}
return elem.(string)
}
func SetInstanceName(ctx context.Context, val string) context.Context {
return context.WithValue(ctx, instanceNameKey, val)
}
func InstanceName(ctx context.Context) string {
elem := ctx.Value(instanceNameKey)
if elem == nil {
return ""
}
return elem.(string)
}
func SetInstancePoolID(ctx context.Context, val string) context.Context {
return context.WithValue(ctx, instancePoolIDKey, val)
}
func InstancePoolID(ctx context.Context) string {
elem := ctx.Value(instancePoolIDKey)
if elem == nil {
return ""
}
return elem.(string)
}
func SetInstancePoolType(ctx context.Context, val string) context.Context {
return context.WithValue(ctx, instancePoolTypeKey, val)
}
func InstancePoolType(ctx context.Context) string {
elem := ctx.Value(instancePoolTypeKey)
if elem == nil {
return ""
}
return elem.(string)
}
func SetInstanceEntity(ctx context.Context, val string) context.Context {
return context.WithValue(ctx, instanceEntityKey, val)
}
func InstanceEntity(ctx context.Context) string {
elem := ctx.Value(instanceEntityKey)
if elem == nil {
return ""
}
return elem.(string)
}
func PopulateInstanceContext(ctx context.Context, instance params.Instance) context.Context {
ctx = SetInstanceID(ctx, instance.ID)
ctx = SetInstanceName(ctx, instance.Name)
ctx = SetInstancePoolID(ctx, instance.PoolID)
return ctx
}
// PopulateContext sets the appropriate fields in the context, based on
// the user object
func PopulateContext(ctx context.Context, user params.User) context.Context {

132
auth/instance_middleware.go Normal file
View file

@ -0,0 +1,132 @@
package auth
import (
"context"
"fmt"
"net/http"
"runner-manager/config"
dbCommon "runner-manager/database/common"
runnerErrors "runner-manager/errors"
"runner-manager/params"
"runner-manager/runner/common"
"strings"
"time"
"github.com/golang-jwt/jwt"
"github.com/pkg/errors"
)
// InstanceJWTClaims holds JWT claims
type InstanceJWTClaims struct {
ID string `json:"id"`
Name string `json:"name"`
PoolID string `json:"provider_id"`
// Scope is either repository or organization
Scope common.PoolType `json:"scope"`
// Entity is the repo or org name
Entity string `json:"entity"`
jwt.StandardClaims
}
func NewInstanceJWTToken(instance params.Instance, secret, entity string, poolType common.PoolType) (string, error) {
// make TTL configurable?
expireToken := time.Now().Add(3 * time.Hour).Unix()
claims := InstanceJWTClaims{
StandardClaims: jwt.StandardClaims{
ExpiresAt: expireToken,
Issuer: "runner-manager",
},
ID: instance.ID,
Name: instance.Name,
PoolID: instance.PoolID,
Scope: poolType,
Entity: entity,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(secret))
if err != nil {
return "", errors.Wrap(err, "signing token")
}
return tokenString, nil
}
// instanceMiddleware is the authentication middleware
// used with gorilla
type instanceMiddleware struct {
store dbCommon.Store
auth *Authenticator
cfg config.JWTAuth
}
// NewjwtMiddleware returns a populated jwtMiddleware
func NewInstanceMiddleware(store dbCommon.Store, cfg config.JWTAuth) (Middleware, error) {
return &instanceMiddleware{
store: store,
cfg: cfg,
}, nil
}
func (amw *instanceMiddleware) claimsToContext(ctx context.Context, claims *InstanceJWTClaims) (context.Context, error) {
if claims == nil {
return ctx, runnerErrors.ErrUnauthorized
}
if claims.Name == "" {
return nil, runnerErrors.ErrUnauthorized
}
instanceInfo, err := amw.store.GetInstanceByName(ctx, claims.Name)
if err != nil {
return ctx, runnerErrors.ErrUnauthorized
}
ctx = PopulateInstanceContext(ctx, instanceInfo)
return ctx, nil
}
// Middleware implements the middleware interface
func (amw *instanceMiddleware) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO: Log error details when authentication fails
ctx := r.Context()
authorizationHeader := r.Header.Get("authorization")
if authorizationHeader == "" {
invalidAuthResponse(w)
return
}
bearerToken := strings.Split(authorizationHeader, " ")
if len(bearerToken) != 2 {
invalidAuthResponse(w)
return
}
claims := &InstanceJWTClaims{}
token, err := jwt.ParseWithClaims(bearerToken[1], claims, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("invalid signing method")
}
return []byte(amw.cfg.Secret), nil
})
if err != nil {
invalidAuthResponse(w)
return
}
if !token.Valid {
invalidAuthResponse(w)
return
}
ctx, err = amw.claimsToContext(ctx, claims)
if err != nil {
invalidAuthResponse(w)
return
}
// ctx = SetJWTClaim(ctx, *claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View file

@ -6,31 +6,15 @@ import (
"fmt"
"net/http"
"strings"
"time"
apiParams "runner-manager/apiserver/params"
"runner-manager/config"
dbCommon "runner-manager/database/common"
runnerErrors "runner-manager/errors"
"runner-manager/params"
"runner-manager/runner/common"
"github.com/golang-jwt/jwt"
"github.com/pkg/errors"
)
// InstanceJWTClaims holds JWT claims
type InstanceJWTClaims struct {
ID string `json:"id"`
Name string `json:"name"`
PoolID string `json:"provider_id"`
// Scope is either repository or organization
Scope common.PoolType `json:"scope"`
// Entity is the repo or org name
Entity string `json:"entity"`
jwt.StandardClaims
}
// JWTClaims holds JWT claims
type JWTClaims struct {
UserID string `json:"user"`
@ -40,29 +24,6 @@ type JWTClaims struct {
jwt.StandardClaims
}
func NewInstanceJWTToken(instance params.Instance, secret, entity string, poolType common.PoolType) (string, error) {
// make TTL configurable?
expireToken := time.Now().Add(3 * time.Hour).Unix()
claims := InstanceJWTClaims{
StandardClaims: jwt.StandardClaims{
ExpiresAt: expireToken,
Issuer: "runner-manager",
},
ID: instance.ID,
Name: instance.Name,
PoolID: instance.PoolID,
Scope: poolType,
Entity: entity,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(secret))
if err != nil {
return "", errors.Wrap(err, "signing token")
}
return tokenString, nil
}
// jwtMiddleware is the authentication middleware
// used with gorilla
type jwtMiddleware struct {

View file

@ -17,22 +17,22 @@ BEARER_TOKEN="{{ .CallbackToken }}"
function call() {
PAYLOAD="$1"
curl -s -X POST -d \'${PAYLOAD}\' -H 'Accept: application/json' -H "Authorization: Bearer ${BEARER_TOKEN}" "${CALLBACK_URL}" || echo "failed to call home: exit code ($?)"
curl -s -X POST -d "${PAYLOAD}" -H 'Accept: application/json' -H "Authorization: Bearer ${BEARER_TOKEN}" "${CALLBACK_URL}" || echo "failed to call home: exit code ($?)"
}
function sendStatus() {
MSG="$1"
call '{"status": "installing", "message": "'$MSG'"}'
call "{\"status\": \"installing\", \"message\": \"$MSG\"}"
}
function success() {
MSG="$1"
call '{"status": "active", "message": "'$MSG'"}'
call "{\"status\": \"idle\", \"message\": \"$MSG\"}"
}
function fail() {
MSG="$1"
call '{"status": "failed", "message": "'$MSG'"}'
call "{\"status\": \"failed\", \"message\": \"$MSG\"}"
exit 1
}

View file

@ -281,3 +281,54 @@ func (c *Client) UpdateRepoPool(repoID, poolID string, param params.UpdatePoolPa
}
return response, nil
}
func (c *Client) ListRepoInstances(repoID string) ([]params.Instance, error) {
url := fmt.Sprintf("%s/api/v1/repositories/%s/instances", c.Config.BaseURL, repoID)
var response []params.Instance
resp, err := c.client.R().
SetResult(&response).
Get(url)
if err != nil || resp.IsError() {
apiErr, decErr := c.decodeAPIError(resp.Body())
if decErr != nil {
return response, errors.Wrap(decErr, "sending request")
}
return response, fmt.Errorf("error performing login: %s", apiErr.Details)
}
return response, nil
}
func (c *Client) ListPoolInstances(poolID string) ([]params.Instance, error) {
url := fmt.Sprintf("%s/api/v1/pools/instances/%s", c.Config.BaseURL, poolID)
var response []params.Instance
resp, err := c.client.R().
SetResult(&response).
Get(url)
if err != nil || resp.IsError() {
apiErr, decErr := c.decodeAPIError(resp.Body())
if decErr != nil {
return response, errors.Wrap(decErr, "sending request")
}
return response, fmt.Errorf("error performing login: %s", apiErr.Details)
}
return response, nil
}
func (c *Client) GetInstanceByName(instanceName string) (params.Instance, error) {
url := fmt.Sprintf("%s/api/v1/instances/%s", c.Config.BaseURL, instanceName)
var response params.Instance
resp, err := c.client.R().
SetResult(&response).
Get(url)
if err != nil || resp.IsError() {
apiErr, decErr := c.decodeAPIError(resp.Body())
if decErr != nil {
return response, errors.Wrap(decErr, "sending request")
}
return response, fmt.Errorf("error performing login: %s", apiErr.Details)
}
return response, nil
}

View file

@ -52,10 +52,10 @@ func init() {
func formatProviders(providers []params.Provider) {
t := table.NewWriter()
header := table.Row{"Name", "Description"}
header := table.Row{"Name", "Description", "Type"}
t.AppendHeader(header)
for _, val := range providers {
t.AppendRow(table.Row{val.Name, val.ProviderType})
t.AppendRow(table.Row{val.Name, val.Description, val.ProviderType})
t.AppendSeparator()
}
fmt.Println(t.Render())

View file

@ -0,0 +1,52 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// repoPoolCmd represents the pool command
var repoInstancesCmd = &cobra.Command{
Use: "runner",
SilenceUsage: true,
Short: "List runners",
Long: `List runners from all pools defined in this repository.`,
Run: nil,
}
var repoRunnerListCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List repository runners",
Long: `List all runners for a given repository.`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if needsInit {
return needsInitError
}
if len(args) == 0 {
return fmt.Errorf("requires a repository ID")
}
if len(args) > 1 {
return fmt.Errorf("too many arguments")
}
instances, err := cli.ListRepoInstances(args[0])
if err != nil {
return err
}
formatInstances(instances)
return nil
},
}
func init() {
repoInstancesCmd.AddCommand(
repoRunnerListCmd,
)
repositoryCmd.AddCommand(repoInstancesCmd)
}

View file

@ -126,6 +126,29 @@ var repoDeleteCmd = &cobra.Command{
},
}
var repoInstanceListCmd = &cobra.Command{
Use: "delete",
Aliases: []string{"remove", "rm", "del"},
Short: "Removes one repository",
Long: `Delete one repository from the manager.`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if needsInit {
return needsInitError
}
if len(args) == 0 {
return fmt.Errorf("requires a repository ID")
}
if len(args) > 1 {
return fmt.Errorf("too many arguments")
}
if err := cli.DeleteRepository(args[0]); err != nil {
return err
}
return nil
},
}
func init() {
repoAddCmd.Flags().StringVar(&repoOwner, "owner", "", "The owner of this repository")

View file

@ -6,7 +6,9 @@ package cmd
import (
"fmt"
"runner-manager/params"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
)
@ -14,28 +16,120 @@ import (
var runnerCmd = &cobra.Command{
Use: "runner",
SilenceUsage: true,
Short: "A brief description of your command",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Short: "List runners in a pool",
Long: `Given a pool ID, of either a repository or an organization,
list all instances.`,
Run: nil,
}
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("runner called")
var runnerListCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List pool runners",
Long: `List all configured pools for a given repository.`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if needsInit {
return needsInitError
}
if len(args) == 0 {
return fmt.Errorf("requires a pool ID")
}
if len(args) > 1 {
return fmt.Errorf("too many arguments")
}
instances, err := cli.ListPoolInstances(args[0])
if err != nil {
return err
}
formatInstances(instances)
return nil
},
}
var runnerShowCmd = &cobra.Command{
Use: "show",
Short: "Show details for a runner",
Long: `Displays a detailed view of a single runner.`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if needsInit {
return needsInitError
}
if len(args) == 0 {
return fmt.Errorf("requires a runner name")
}
if len(args) > 1 {
return fmt.Errorf("too many arguments")
}
instance, err := cli.GetInstanceByName(args[0])
if err != nil {
return err
}
formatSingleInstance(instance)
return nil
},
}
func init() {
runnerCmd.AddCommand(
runnerListCmd,
runnerShowCmd,
)
rootCmd.AddCommand(runnerCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// runnerCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// runnerCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
func formatInstances(param []params.Instance) {
t := table.NewWriter()
header := table.Row{"Name", "Status", "Runner Status", "Pool ID"}
t.AppendHeader(header)
for _, inst := range param {
t.AppendRow(table.Row{inst.Name, inst.Status, inst.RunnerStatus, inst.PoolID})
t.AppendSeparator()
}
fmt.Println(t.Render())
}
func formatSingleInstance(instance params.Instance) {
t := table.NewWriter()
header := table.Row{"Field", "Value"}
t.AppendHeader(header)
t.AppendRow(table.Row{"ID", instance.ID}, table.RowConfig{AutoMerge: false})
t.AppendRow(table.Row{"Provider ID", instance.ProviderID}, table.RowConfig{AutoMerge: false})
t.AppendRow(table.Row{"Name", instance.Name}, table.RowConfig{AutoMerge: false})
t.AppendRow(table.Row{"OS Type", instance.OSType}, table.RowConfig{AutoMerge: false})
t.AppendRow(table.Row{"OS Architecture", instance.OSArch}, table.RowConfig{AutoMerge: false})
t.AppendRow(table.Row{"OS Name", instance.OSName}, table.RowConfig{AutoMerge: false})
t.AppendRow(table.Row{"OS Version", instance.OSVersion}, table.RowConfig{AutoMerge: false})
t.AppendRow(table.Row{"Status", instance.Status}, table.RowConfig{AutoMerge: false})
t.AppendRow(table.Row{"Runner Status", instance.RunnerStatus}, table.RowConfig{AutoMerge: false})
t.AppendRow(table.Row{"Pool ID", instance.PoolID}, table.RowConfig{AutoMerge: false})
if len(instance.Addresses) > 0 {
for _, addr := range instance.Addresses {
t.AppendRow(table.Row{"Addresses", addr}, table.RowConfig{AutoMerge: true})
}
}
if len(instance.StatusMessages) > 0 {
for _, msg := range instance.StatusMessages {
t.AppendRow(table.Row{"Status Updates", fmt.Sprintf("%s: %s", msg.CreatedAt.Format("2006-01-02T15:04:05"), msg.Message)}, table.RowConfig{AutoMerge: true})
}
}
t.SetColumnConfigs([]table.ColumnConfig{
{Number: 1, AutoMerge: true},
{Number: 2, AutoMerge: false},
})
fmt.Println(t.Render())
}

View file

@ -90,6 +90,11 @@ func main() {
log.Fatalf("failed to create controller: %+v", err)
}
instanceMiddleware, err := auth.NewInstanceMiddleware(db, cfg.JWTAuth)
if err != nil {
log.Fatal(err)
}
jwtMiddleware, err := auth.NewjwtMiddleware(db, cfg.JWTAuth)
if err != nil {
log.Fatal(err)
@ -100,7 +105,7 @@ func main() {
log.Fatal(err)
}
router := routers.NewAPIRouter(controller, logWriter, jwtMiddleware, initMiddleware)
router := routers.NewAPIRouter(controller, logWriter, jwtMiddleware, initMiddleware, instanceMiddleware)
tlsCfg, err := cfg.APIServer.APITLSConfig()
if err != nil {

View file

@ -165,6 +165,7 @@ func (g *Github) Validate() error {
type Provider struct {
Name string `toml:"name" json:"name"`
ProviderType ProviderType `toml:"provider_type" json:"provider-type"`
Description string `toml:"description" json:"description"`
LXD LXD `toml:"lxd" json:"lxd"`
}

View file

@ -47,6 +47,7 @@ type Store interface {
// GetInstance(ctx context.Context, poolID string, instanceID string) (params.Instance, error)
GetPoolInstanceByName(ctx context.Context, poolID string, instanceName string) (params.Instance, error)
GetInstanceByName(ctx context.Context, instanceName string) (params.Instance, error)
AddInstanceStatusMessage(ctx context.Context, instanceID string, statusMessage string) error
GetUser(ctx context.Context, user string) (params.User, error)
GetUserByID(ctx context.Context, userID string) (params.User, error)

View file

@ -83,6 +83,18 @@ type Address struct {
Address string
Type string
InstanceID uuid.UUID
Instance Instance `gorm:"foreignKey:InstanceID"`
}
type InstanceStatusUpdate struct {
Base
Message string `gorm:"type:text"`
InstanceID uuid.UUID
Instance Instance `gorm:"foreignKey:InstanceID"`
}
type Instance struct {
@ -94,13 +106,15 @@ type Instance struct {
OSArch config.OSArch
OSName string
OSVersion string
Addresses []Address `gorm:"foreignKey:id"`
Addresses []Address `gorm:"foreignKey:InstanceID"`
Status common.InstanceStatus
RunnerStatus common.RunnerStatus
CallbackURL string
PoolID uuid.UUID
Pool Pool `gorm:"foreignKey:PoolID"`
StatusMessages []InstanceStatusUpdate `gorm:"foreignKey:InstanceID"`
}
type User struct {

View file

@ -45,6 +45,7 @@ func (s *sqlDatabase) migrateDB() error {
&Repository{},
&Organization{},
&Address{},
&InstanceStatusUpdate{},
&Instance{},
&ControllerInfo{},
&User{},
@ -470,7 +471,9 @@ func (s *sqlDatabase) CreateRepositoryPool(ctx context.Context, repoId string, p
}
for _, tt := range tags {
s.conn.Model(&newPool).Association("Tags").Append(&tt)
if err := s.conn.Model(&newPool).Association("Tags").Append(&tt); err != nil {
return params.Pool{}, errors.Wrap(err, "saving tag")
}
}
pool, err := s.getPoolByID(ctx, newPool.ID.String(), "Tags")
@ -783,22 +786,30 @@ func (s *sqlDatabase) sqlToParamsInstance(instance Instance) params.Instance {
id = *instance.ProviderID
}
ret := params.Instance{
ID: instance.ID.String(),
ProviderID: id,
Name: instance.Name,
OSType: instance.OSType,
OSName: instance.OSName,
OSVersion: instance.OSVersion,
OSArch: instance.OSArch,
Status: instance.Status,
RunnerStatus: instance.RunnerStatus,
PoolID: instance.PoolID.String(),
CallbackURL: instance.CallbackURL,
ID: instance.ID.String(),
ProviderID: id,
Name: instance.Name,
OSType: instance.OSType,
OSName: instance.OSName,
OSVersion: instance.OSVersion,
OSArch: instance.OSArch,
Status: instance.Status,
RunnerStatus: instance.RunnerStatus,
PoolID: instance.PoolID.String(),
CallbackURL: instance.CallbackURL,
StatusMessages: []params.StatusMessage{},
}
for _, addr := range instance.Addresses {
ret.Addresses = append(ret.Addresses, s.sqlAddressToParamsAddress(addr))
}
for _, msg := range instance.StatusMessages {
ret.StatusMessages = append(ret.StatusMessages, params.StatusMessage{
CreatedAt: msg.CreatedAt,
Message: msg.Message,
})
}
return ret
}
@ -864,9 +875,18 @@ func (s *sqlDatabase) getPoolInstanceByName(ctx context.Context, poolID string,
return instance, nil
}
func (s *sqlDatabase) getInstanceByName(ctx context.Context, instanceName string) (Instance, error) {
func (s *sqlDatabase) getInstanceByName(ctx context.Context, instanceName string, preload ...string) (Instance, error) {
var instance Instance
q := s.conn.Model(&Instance{}).
q := s.conn
if len(preload) > 0 {
for _, item := range preload {
q = q.Preload(item)
}
}
q = q.Model(&Instance{}).
Preload(clause.Associations).
Where("name = ?", instanceName).
First(&instance)
@ -885,7 +905,7 @@ func (s *sqlDatabase) GetPoolInstanceByName(ctx context.Context, poolID string,
}
func (s *sqlDatabase) GetInstanceByName(ctx context.Context, instanceName string) (params.Instance, error) {
instance, err := s.getInstanceByName(ctx, instanceName)
instance, err := s.getInstanceByName(ctx, instanceName, "StatusMessages")
if err != nil {
return params.Instance{}, errors.Wrap(err, "fetching instance")
}
@ -906,6 +926,22 @@ func (s *sqlDatabase) DeleteInstance(ctx context.Context, poolID string, instanc
return nil
}
func (s *sqlDatabase) AddInstanceStatusMessage(ctx context.Context, instanceID string, statusMessage string) error {
instance, err := s.getInstanceByID(ctx, instanceID)
if err != nil {
return errors.Wrap(err, "updating instance")
}
msg := InstanceStatusUpdate{
Message: statusMessage,
}
if err := s.conn.Model(&instance).Association("StatusMessages").Append(&msg); err != nil {
return errors.Wrap(err, "adding status message")
}
return nil
}
func (s *sqlDatabase) UpdateInstance(ctx context.Context, instanceID string, param params.UpdateInstanceParams) (params.Instance, error) {
instance, err := s.getInstanceByID(ctx, instanceID)
if err != nil {

View file

@ -21,6 +21,11 @@ type Address struct {
Type AddressType `json:"type"`
}
type StatusMessage struct {
CreatedAt time.Time `json:"created_at"`
Message string `json:"message"`
}
type Instance struct {
// ID is the database ID of this instance.
ID string `json:"id"`
@ -50,6 +55,8 @@ type Instance struct {
RunnerStatus common.RunnerStatus `json:"runner_status"`
PoolID string `json:"pool_id"`
StatusMessages []StatusMessage `json:"status_messages,omitempty"`
// Do not serialize sensitive info.
CallbackURL string `json:"-"`
}
@ -157,4 +164,5 @@ type GithubCredentials struct {
type Provider struct {
Name string `json:"name"`
ProviderType config.ProviderType `json:"type"`
Description string `json:"description"`
}

View file

@ -148,3 +148,8 @@ type UpdateRepositoryParams struct {
CredentialsName string `json:"credentials_name"`
WebhookSecret string `json:"webhook_secret"`
}
type InstanceUpdateMessage struct {
Status common.RunnerStatus `json:"status"`
Message string `json:"message"`
}

View file

@ -382,7 +382,7 @@ func (r *Repository) updateArgsFromProviderInstance(providerInstance params.Inst
return params.UpdateInstanceParams{
ProviderID: providerInstance.ProviderID,
OSName: providerInstance.OSName,
OSVersion: providerInstance.OSName,
OSVersion: providerInstance.OSVersion,
Addresses: providerInstance.Addresses,
Status: providerInstance.Status,
RunnerStatus: providerInstance.RunnerStatus,
@ -464,8 +464,6 @@ func (r *Repository) addInstanceToProvider(instance params.Instance) error {
return nil
}
// TODO: add function to set runner status to idle when instance calls home on callback url
func (r *Repository) AddRunner(ctx context.Context, poolID string) error {
pool, err := r.store.GetRepositoryPool(r.ctx, r.id, poolID)
if err != nil {
@ -484,11 +482,23 @@ func (r *Repository) AddRunner(ctx context.Context, poolID string) error {
CallbackURL: r.cfg.Internal.InstanceCallbackURL,
}
_, err = r.store.CreateInstance(r.ctx, poolID, createParams)
instance, err := r.store.CreateInstance(r.ctx, poolID, createParams)
if err != nil {
return errors.Wrap(err, "creating instance")
}
updateParams := params.UpdateInstanceParams{
OSName: instance.OSName,
OSVersion: instance.OSVersion,
Addresses: instance.Addresses,
Status: instance.Status,
ProviderID: instance.ProviderID,
}
if _, err := r.store.UpdateInstance(r.ctx, instance.ID, updateParams); err != nil {
return errors.Wrap(err, "updating runner state")
}
return nil
}
@ -656,20 +666,3 @@ func (r *Repository) HandleWorkflowJob(job params.WorkflowJob) error {
}
return nil
}
func (r *Repository) ListInstances() ([]params.Instance, error) {
return nil, nil
}
func (r *Repository) GetInstance() (params.Instance, error) {
return params.Instance{}, nil
}
func (r *Repository) DeleteInstance() error {
return nil
}
func (r *Repository) StopInstance() error {
return nil
}
func (r *Repository) StartInstance() error {
return nil
}

View file

@ -235,6 +235,7 @@ func (l *LXD) AsParams() params.Provider {
return params.Provider{
Name: l.cfg.Name,
ProviderType: l.cfg.ProviderType,
Description: l.cfg.Description,
}
}

View file

@ -283,8 +283,16 @@ func (r *Runner) ListRepoPools(ctx context.Context, repoID string) ([]params.Poo
return pools, nil
}
func (r *Runner) ListPoolInstances(ctx context.Context) error {
return nil
func (r *Runner) ListPoolInstances(ctx context.Context, poolID string) ([]params.Instance, error) {
if !auth.IsAdmin(ctx) {
return nil, runnerErrors.ErrUnauthorized
}
instances, err := r.store.ListInstances(ctx, poolID)
if err != nil {
return []params.Instance{}, errors.Wrap(err, "fetching instances")
}
return instances, nil
}
func (r *Runner) loadRepoPoolManager(repo params.Repository) (common.PoolManager, error) {
@ -345,3 +353,53 @@ func (r *Runner) UpdateRepoPool(ctx context.Context, repoID, poolID string, para
}
return newPool, nil
}
func (r *Runner) ListRepoInstances(ctx context.Context, repoID string) ([]params.Instance, error) {
if !auth.IsAdmin(ctx) {
return nil, runnerErrors.ErrUnauthorized
}
instances, err := r.store.ListRepoInstances(ctx, repoID)
if err != nil {
return []params.Instance{}, errors.Wrap(err, "fetching instances")
}
return instances, nil
}
// TODO: move these in another file
func (r *Runner) GetInstance(ctx context.Context, instanceName string) (params.Instance, error) {
if !auth.IsAdmin(ctx) {
return params.Instance{}, runnerErrors.ErrUnauthorized
}
instance, err := r.store.GetInstanceByName(ctx, instanceName)
if err != nil {
return params.Instance{}, errors.Wrap(err, "fetching instance")
}
return instance, nil
}
func (r *Runner) AddInstanceStatusMessage(ctx context.Context, param params.InstanceUpdateMessage) error {
instanceID := auth.InstanceID(ctx)
if instanceID == "" {
return runnerErrors.ErrUnauthorized
}
if err := r.store.AddInstanceStatusMessage(ctx, instanceID, param.Message); err != nil {
return errors.Wrap(err, "adding status update")
}
// if param.Status == providerCommon.RunnerIdle {
// }
updateParams := params.UpdateInstanceParams{
RunnerStatus: param.Status,
}
if _, err := r.store.UpdateInstance(r.ctx, instanceID, updateParams); err != nil {
return errors.Wrap(err, "updating runner state")
}
return nil
}

View file

@ -205,7 +205,7 @@ func GetCloudConfig(bootstrapParams params.BootstrapInstance, tools github.Runne
cloudCfg.AddSSHKey(bootstrapParams.SSHKeys...)
cloudCfg.AddFile(installScript, "/install_runner.sh", "root:root", "755")
cloudCfg.AddRunCmd("/install_runner.sh")
cloudCfg.AddRunCmd("rm -f /install_runner.sh")
// cloudCfg.AddRunCmd("rm -f /install_runner.sh")
asStr, err := cloudCfg.Serialize()
if err != nil {