From 79618bde38c03833c43c7c36f6b2e64ead420d39 Mon Sep 17 00:00:00 2001 From: Nathan Pierce Date: Mon, 21 Oct 2024 18:48:53 -0500 Subject: [PATCH] added new global variables and ENVs for database + cleanup --- Makefile | 3 +- README.md | 52 ++++++++++++---------------- VERSION | 2 +- docker/docker-compose.yml | 13 +++++-- internal/config/config.go | 48 +++++++++++++++++++------- internal/database/database.go | 5 ++- main.go | 55 ++++++++++++++++++++++++++---- plugins/handlers/github/README.md | 16 ++++----- plugins/receivers/github/README.md | 13 ++++--- plugins/receivers/github/github.go | 16 ++++----- 10 files changed, 141 insertions(+), 82 deletions(-) diff --git a/Makefile b/Makefile index 6c0dc29..65b5ae9 100644 --- a/Makefile +++ b/Makefile @@ -51,10 +51,9 @@ build-linux: GOOS=linux OS_TYPE=linux $(MAKE) go.build run-docker-compose: - cp dist/anklet_v$(VERSION)*_linux_$(ARCH).zip docker/ + cp dist/anklet_v$(VERSION)*_linux_$(ARCH) docker/ cd docker && \ rm -f anklet_linux_$(ARCH) && \ - unzip anklet_v$(VERSION)*_linux_$(ARCH).zip && \ mv anklet_v$(VERSION)*_linux_$(ARCH) anklet_linux_$(ARCH) && \ rm -f anklet_v$(VERSION)*_linux_$(ARCH).zip && \ docker-compose up --build --force-recreate diff --git a/README.md b/README.md index 3900f61..651c689 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,12 @@ With the github plugin, there is a Receiver Plugin and a Handler Plugin. work_dir: /tmp/ pid_file_dir: /tmp/ # plugins_path: ~/.config/anklet/plugins/ + global_database_url: localhost + global_database_port: 6379 + global_database_user: "" + global_database_password: "" + global_database_database: 0 + global_private_key: /Users/nathanpierce/veertuinc-anklet.2024-07-19.private-key.pem log: # if file_dir is not set, it will be set to current directory you execute anklet in file_dir: /Users/myUser/Library/Logs/ @@ -86,48 +92,32 @@ With the github plugin, there is a Receiver Plugin and a Handler Plugin. hook_id: 489747753 port: 54321 # port that's open to the internet so github can post to it secret: 00000000 - private_key: /Users/nathanpierce/veertuinc-anklet.2024-07-19.private-key.pem + #private_key: /Users/nathanpierce/veertuinc-anklet.2024-07-19.private-key.pem app_id: 949431 installation_id: 52970581 repo: anklet owner: veertuinc - database: - enabled: true - url: localhost - port: 6379 - user: "" - password: "" - database: 0 + #database: + #url: localhost + #port: 6379 + #user: "" + #password: "" + #database: 0 # GITHUB HANDLERS - name: RUNNER1 plugin: github - private_key: /Users/nathanpierce/veertuinc-anklet.2024-07-19.private-key.pem app_id: 949431 installation_id: 52970581 repo: anklet owner: veertuinc registry_url: http://anka.registry:8089 sleep_interval: 10 # sleep 10 seconds between checks for new jobs - database: - enabled: true - url: localhost - port: 6379 - user: "" - password: "" - database: 0 - name: RUNNER2 plugin: github token: github_pat_1XXXXX repo: anklet owner: veertuinc registry_url: http://anka.registry:8089 - database: - enabled: true - url: localhost - port: 6379 - user: "" - password: "" - database: 0 ``` > Note: You can only ever run two VMs per host per the Apple macOS SLA. While you can specify more than two plugins, only two will ever be running a VM at one time. `sleep_interval` can be used to control the frequency/priority of a plugin and increase the odds that a job will be picked up. @@ -158,6 +148,10 @@ tail -fF /opt/homebrew/var/log/redis.log For production, we recommend running a [redis cluster](https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec/) on infrastructure that is separate from your Anklet hosts and has guaranteed uptime. +Your config.yml file must define the database in one of the following ways: +- Using the `database` variables (under each plugin). +- Using the `global_database_*` variables (applies to and overrides the `database` variables under each plugin). + ### Plugin Setup and Usage Guides You can control the location plugins are stored on the host by setting the `plugins_path` in the `config.yml` file. If not set, it will default to `~/.config/anklet/plugins/`. @@ -298,6 +292,11 @@ pid_file_dir: /tmp/ log: # if file_dir is not set, it will be set to current directory you execute anklet in file_dir: /Users/nathanpierce/Library/Logs/ +global_database_url: localhost +global_database_port: 6379 +global_database_user: "" +global_database_password: "" +global_database_database: 0 metrics: aggregator: true metrics_urls: @@ -306,13 +305,6 @@ metrics: - http://192.168.1.203:8080/metrics port: 8081 # port to serve aggregator on sleep_interval: 10 # how often to fetch metrics from each Anklet defined - database: - enabled: true - url: localhost - port: 6379 - user: "" - password: "" - database: 0 ``` You can see that this requires a database to be running. The aggregator will store the metrics in Redis so that it can serve them up without delay. diff --git a/VERSION b/VERSION index bcaffe1..8adc70f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.7.0 \ No newline at end of file +0.8.0 \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 58928f6..405d731 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -8,16 +8,17 @@ services: container_name: anklet volumes: - /Users/nathanpierce/.config/anklet:/root/.config/anklet/ + - /Users/nathanpierce/veertuinc-anklet.2024-10-09.private-key.pem:/private-key.pem ports: - "8081:8081" environment: - # - LOG_LEVEL=debug + # - LOG_LEVEL=dev - ANKLET_CONFIG_FILE_NAME=org-receiver-config.yml ################################################################################## # ANKLET_GLOBAL_PRIVATE_KEY allows you to use the same private key for all runners # instead of defining a different one in the config.yml file for each runner. # You can also set it to be the PEM contents, vs pointing to a file. - # - ANKLET_GLOBAL_PRIVATE_KEY=/Users/nathanpierce/veertuinc-anklet.2024-07-19.private-key.pem + - ANKLET_GLOBAL_PRIVATE_KEY=/private-key.pem # - |- # ANKLET_GLOBAL_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY----- # MIIEpAIBAAKCAQEAyqZDXOEzV5gRocWAhH73chGjn4HBh1UCG2Du4v9JmcjfyYT3 @@ -35,4 +36,10 @@ services: # - ANKLET_METRICS_DATABASE_USER= # - ANKLET_METRICS_DATABASE_PASSWORD= # - ANKLET_METRICS_DATABASE_DATABASE=0 - + ################################################################################## + # GLOBAL DATABASE ENVs: if set, they will be used instead of the definitions in the config .yml file + - ANKLET_GLOBAL_DATABASE_URL=host.docker.internal + - ANKLET_GLOBAL_DATABASE_PORT=6379 + - ANKLET_GLOBAL_DATABASE_USER= + - ANKLET_GLOBAL_DATABASE_PASSWORD= + - ANKLET_GLOBAL_DATABASE_DATABASE=0 diff --git a/internal/config/config.go b/internal/config/config.go index e577510..0458c8c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,14 +15,19 @@ import ( type ContextKey string type Config struct { - Plugins []Plugin `yaml:"plugins"` - Log Log `yaml:"log"` - PidFileDir string `yaml:"pid_file_dir"` - LogFileDir string `yaml:"log_file_dir"` - WorkDir string `yaml:"work_dir"` - Metrics Metrics `yaml:"metrics"` - GlobalPrivateKey string `yaml:"global_private_key"` - PluginsPath string `yaml:"plugins_path"` + Plugins []Plugin `yaml:"plugins"` + Log Log `yaml:"log"` + PidFileDir string `yaml:"pid_file_dir"` + LogFileDir string `yaml:"log_file_dir"` + WorkDir string `yaml:"work_dir"` + Metrics Metrics `yaml:"metrics"` + GlobalPrivateKey string `yaml:"global_private_key"` + PluginsPath string `yaml:"plugins_path"` + GlobalDatabaseURL string `yaml:"global_database_url"` + GlobalDatabasePort int `yaml:"global_database_port"` + GlobalDatabaseUser string `yaml:"global_database_user"` + GlobalDatabasePassword string `yaml:"global_database_password"` + GlobalDatabaseDatabase int `yaml:"global_database_database"` } type Log struct { @@ -43,7 +48,6 @@ type Database struct { User string `yaml:"user"` Password string `yaml:"password"` Database int `yaml:"database"` - Enabled bool `yaml:"enabled"` } type Workflow struct { @@ -110,10 +114,6 @@ func LoadInEnvs(config Config) (Config, error) { } config.Metrics.SleepInterval = value } - envDBEnabled := os.Getenv("ANKLET_METRICS_DATABASE_ENABLED") - if envDBEnabled != "" { - config.Metrics.Database.Enabled = envDBEnabled == "true" - } envDBUser := os.Getenv("ANKLET_METRICS_DATABASE_USER") if envDBUser != "" { config.Metrics.Database.User = envDBUser @@ -152,6 +152,28 @@ func LoadInEnvs(config Config) (Config, error) { if envGlobalPrivateKey != "" { config.GlobalPrivateKey = envGlobalPrivateKey } + + envGlobalDatabaseURL := os.Getenv("ANKLET_GLOBAL_DATABASE_URL") + if envGlobalDatabaseURL != "" { + config.GlobalDatabaseURL = envGlobalDatabaseURL + } + envGlobalDatabasePort := os.Getenv("ANKLET_GLOBAL_DATABASE_PORT") + if envGlobalDatabasePort != "" { + port, err := strconv.Atoi(envGlobalDatabasePort) + if err != nil { + return Config{}, err + } + config.GlobalDatabasePort = port + } + envGlobalDatabaseUser := os.Getenv("ANKLET_GLOBAL_DATABASE_USER") + if envGlobalDatabaseUser != "" { + config.GlobalDatabaseUser = envGlobalDatabaseUser + } + envGlobalDatabasePassword := os.Getenv("ANKLET_GLOBAL_DATABASE_PASSWORD") + if envGlobalDatabasePassword != "" { + config.GlobalDatabasePassword = envGlobalDatabasePassword + } + // pidFileDir := os.Getenv("ANKLET_PID_FILE_DIR") // if pidFileDir != "" { // config.PidFileDir = pidFileDir diff --git a/internal/database/database.go b/internal/database/database.go index cc2c9fb..0609f79 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -30,9 +30,8 @@ func NewClient(ctx context.Context, config config.Database) (*Database, error) { logging.DevDebug(ctx, "pinging redis client") ping := rdb.Ping(ctx) - if ping.Err() != nil { - logging.DevDebug(ctx, fmt.Sprintf("error pinging redis client: %v", ping.Err())) - return nil, ping.Err() + if ping.Err() != nil && ping.Err().Error() != "" { + return nil, errors.New("error pinging redis client: " + ping.Err().Error()) } pong, err := ping.Result() if err != nil { diff --git a/main.go b/main.go index 4e6c4b4..0557b2a 100644 --- a/main.go +++ b/main.go @@ -232,6 +232,21 @@ func worker(parentCtx context.Context, logger *slog.Logger, loadedConfig config. } } }() + + // set global database variables + var databaseURL string + var databasePort int + var databaseUser string + var databasePassword string + var databaseDatabase int + if loadedConfig.GlobalDatabaseURL != "" { + databaseURL = loadedConfig.GlobalDatabaseURL + databasePort = loadedConfig.GlobalDatabasePort + databaseUser = loadedConfig.GlobalDatabaseUser + databasePassword = loadedConfig.GlobalDatabasePassword + databaseDatabase = loadedConfig.GlobalDatabaseDatabase + } + // Setup Metrics Server and context metricsPort := "8080" if loadedConfig.Metrics.Port != "" { @@ -246,7 +261,20 @@ func worker(parentCtx context.Context, logger *slog.Logger, loadedConfig config. metricsService := metrics.NewServer(metricsPort) if loadedConfig.Metrics.Aggregator { workerCtx = logging.AppendCtx(workerCtx, slog.Any("metrics_urls", loadedConfig.Metrics.MetricsURLs)) - databaseContainer, err := database.NewClient(workerCtx, loadedConfig.Metrics.Database) + if databaseURL == "" { // if no global database URL is set, use the metrics database URL + databaseURL = loadedConfig.Metrics.Database.URL + databasePort = loadedConfig.Metrics.Database.Port + databaseUser = loadedConfig.Metrics.Database.User + databasePassword = loadedConfig.Metrics.Database.Password + databaseDatabase = loadedConfig.Metrics.Database.Database + } + databaseContainer, err := database.NewClient(workerCtx, config.Database{ + URL: databaseURL, + Port: databasePort, + User: databaseUser, + Password: databasePassword, + Database: databaseDatabase, + }) if err != nil { panic(fmt.Sprintf("unable to access database: %v", err)) } @@ -326,12 +354,11 @@ func worker(parentCtx context.Context, logger *slog.Logger, loadedConfig config. if plugin.Repo == "" { logger.InfoContext(pluginCtx, "no repo set for plugin; assuming it's an organization level plugin") - logging.DevDebug(pluginCtx, "setting isRepoSet to false") pluginCtx = context.WithValue(pluginCtx, config.ContextKey("isRepoSet"), false) logging.DevDebug(pluginCtx, "set isRepoSet to false") } else { pluginCtx = context.WithValue(pluginCtx, config.ContextKey("isRepoSet"), true) - logging.DevDebug(pluginCtx, "setting isRepoSet to true") + logging.DevDebug(pluginCtx, "set isRepoSet to true") } if plugin.PrivateKey == "" && loadedConfig.GlobalPrivateKey != "" { @@ -354,12 +381,26 @@ func worker(parentCtx context.Context, logger *slog.Logger, loadedConfig config. logging.DevDebug(pluginCtx, "loaded the anka CLI") } - if plugin.Database.Enabled { + if databaseURL != "" || plugin.Database.URL != "" { + if databaseURL == "" { + databaseURL = plugin.Database.URL + databasePort = plugin.Database.Port + databaseUser = plugin.Database.User + databasePassword = plugin.Database.Password + databaseDatabase = plugin.Database.Database + } logging.DevDebug(pluginCtx, "connecting to database") - databaseClient, err := database.NewClient(pluginCtx, plugin.Database) + databaseClient, err := database.NewClient(pluginCtx, config.Database{ + URL: databaseURL, + Port: databasePort, + User: databaseUser, + Password: databasePassword, + Database: databaseDatabase, + }) if err != nil { - panic(fmt.Sprintf("unable to access database: %v", err)) + panic("unable to access database: " + err.Error()) } + logger.InfoContext(pluginCtx, "connected to database", slog.Any("database", databaseClient)) pluginCtx = context.WithValue(pluginCtx, config.ContextKey("database"), databaseClient) logging.DevDebug(pluginCtx, "connected to database") } @@ -378,7 +419,6 @@ func worker(parentCtx context.Context, logger *slog.Logger, loadedConfig config. logging.DevDebug(pluginCtx, "plugin for loop::default") run.Plugin(workerCtx, pluginCtx, pluginCancel, logger, firstPluginStarted, metricsData) if workerCtx.Err() != nil || toRunOnce == "true" { - logging.DevDebug(pluginCtx, "plugin for loop::default::workerCtx.Err() != nil || toRunOnce == \"true\"") pluginCancel() logger.WarnContext(pluginCtx, shutDownMessage) return @@ -403,6 +443,7 @@ func worker(parentCtx context.Context, logger *slog.Logger, loadedConfig config. go metricsService.Start(workerCtx, logger, soloReceiver) } wg.Wait() + time.Sleep(time.Second) // prevents exiting before the logger has a chance to write the final log entry (from panics) logger.WarnContext(workerCtx, "anklet (and all plugins) shut down") os.Exit(0) } diff --git a/plugins/handlers/github/README.md b/plugins/handlers/github/README.md index eb4ebb4..ae53e3b 100644 --- a/plugins/handlers/github/README.md +++ b/plugins/handlers/github/README.md @@ -23,17 +23,17 @@ plugins: registry_url: http://anka.registry:8089 runner_group: macOS # requires Enterprise github # sleep_interval: 5 # Optional; defaults to 1 second. - database: - enabled: true - url: localhost - port: 6379 - user: "" - password: "" - database: 0 + #database: + # enabled: true + # url: localhost + # port: 6379 + # user: "" + # password: "" + # database: 0 ``` - Your PAT or Github App must have **Actions** and **Administration** Read & Write permissions. -- The `database` is required. You can find installation instructions in the anklet main [README.md](../../README.md#database-setup). +- You must define the database in the config.yml file either using the `database` section or the `global_database_*` variables. You can find installation instructions in the anklet main [README.md](../../README.md#database-setup). - If you are attempting to register runners for an entire organization, do NOT set `repo` and make sure your Github App has `Self-hosted runners` > `Read and write` permissions. - If your Organization level runner is registered and your public repo jobs are not picking it up even though the labels are a perfect match, make sure the Runner groups (likely `Default`) has `Allow public repositories`. diff --git a/plugins/receivers/github/README.md b/plugins/receivers/github/README.md index 6fa19d3..ca667bb 100644 --- a/plugins/receivers/github/README.md +++ b/plugins/receivers/github/README.md @@ -23,13 +23,12 @@ plugins: repo: anklet owner: veertuinc skip_redeliver: true - database: - enabled: true - url: localhost - port: 6379 - user: "" - password: "" - database: 0 + #database: + # url: localhost + # port: 6379 + # user: "" + # password: "" + # database: 0 ``` - If you leave off `repo`, the receiver will be an organization level receiver. diff --git a/plugins/receivers/github/github.go b/plugins/receivers/github/github.go index c51101b..b0feb66 100644 --- a/plugins/receivers/github/github.go +++ b/plugins/receivers/github/github.go @@ -52,8 +52,7 @@ func exists_in_array_exact(array_to_search_in []string, desired []string) bool { func InQueue(pluginCtx context.Context, logger *slog.Logger, jobID int64, queue string) (bool, error) { databaseContainer, err := database.GetDatabaseFromContext(pluginCtx) if err != nil { - logger.ErrorContext(pluginCtx, "error getting database client from context", "error", err) - return false, err + logging.Panic(pluginCtx, pluginCtx, "error getting database client from context: "+err.Error()) } queued, err := databaseContainer.Client.LRange(pluginCtx, queue, 0, -1).Result() if err != nil { @@ -147,8 +146,7 @@ func Run( databaseContainer, err := database.GetDatabaseFromContext(pluginCtx) if err != nil { - logger.ErrorContext(pluginCtx, "error getting database client from context", "error", err) - return + logging.Panic(pluginCtx, pluginCtx, "error getting database client from context: "+err.Error()) } rateLimiter := internalGithub.GetRateLimitWaiterClientFromContext(pluginCtx) httpTransport := config.GetHttpTransportFromContext(pluginCtx) @@ -164,8 +162,11 @@ func Run( } itr, err := ghinstallation.New(httpTransport, int64(ctxPlugin.AppID), int64(ctxPlugin.InstallationID), privateKey) if err != nil { - logger.ErrorContext(pluginCtx, "error creating github app installation token", "err", err) - return + if strings.Contains(err.Error(), "invalid key") { + logging.Panic(pluginCtx, pluginCtx, "error creating github app installation token: "+err.Error()+" (does the key exist on the filesystem?)") + } else { + logging.Panic(pluginCtx, pluginCtx, "error creating github app installation token: "+err.Error()) + } } rateLimiter.Transport = itr githubClient = github.NewClient(rateLimiter) @@ -181,8 +182,7 @@ func Run( http.HandleFunc("/jobs/v1/receiver", func(w http.ResponseWriter, r *http.Request) { databaseContainer, err := database.GetDatabaseFromContext(pluginCtx) if err != nil { - logger.ErrorContext(pluginCtx, "error getting database client from context", "error", err) - return + logging.Panic(pluginCtx, pluginCtx, "error getting database client from context: "+err.Error()) } payload, err := github.ValidatePayload(r, []byte(ctxPlugin.Secret)) if err != nil {