diff --git a/README.md b/README.md index 317644b..87c62c3 100644 --- a/README.md +++ b/README.md @@ -81,16 +81,39 @@ vars: FOO: bar BAR: foo ``` -### Environment variables - -Environment variables used by our worker are always prefixed with `RHC_WORKER_`. - -Use below variables to adjust worker behavior. -* Related to logging - * `RHC_WORKER_LOG_FOLDER` - default is `"/var/log/rhc-worker-bash"` - * `RHC_WORKER_LOG_FILENAME` - default is `"rhc-worker-bash.log"` -* Related to verification of yaml file containing bash script - * `RHC_WORKER_GPG_CHECK` - default is `"1"` - * `RHC_WORKER_VERIFY_YAML` - default is `"1"` -* Related to script temporary location and execution - * `RHC_WORKER_TMP_DIR` - default is `"/var/lib/rhc-worker-bash"` +## FAQ + +### Are there special environment variables that worker uses? + +There is one special variable that must be set in order to run our worker and that is `YGG_SOCKET_ADDR`, this variable value is set by `rhcd` via `--socket-addr` option. + +Other than that there are no special variables, however if executed bash script contained some `content_vars` (like the example above), then during the execution of the script are all environment variables always prefixed with `RHC_WORKER_`and unset after the bash script completes. + +### Can I somehow change behavior of worker? e.g. different destination for logs? + +Yes, some values can be changed if config exists at `/etc/rhc/workers/rhc-worker-bash.yml`, **the config must have valid yaml format**, see all available fields below. + +Example of full config (with default values): +```yaml +# rhc-worker-bash configuration + +# recipient directive to register with dispatcher +directive: "rhc-worker-bash" + +# whether to verify incoming yaml files +verify_yaml: true + +# perform the insights-client GPG check on the insights-core egg +insights_core_gpg_check: true + +# temporary directory in which the temporary files with executed bash scripts are created +temporary_worker_directory: "/var/lib/rhc-worker-bash" + +# Options to adjust name and directory for worker logs +log_dir: "/var/log/rhc-worker-bash" +log_filename: "rhc-worker-bash.log" +``` + +### Can I change the location of rhc-worker bash config? + +No, not right now. If you want this feature please create an issue or upvote already existing issue. diff --git a/src/main.go b/src/main.go index 9f71e42..dd5c50f 100644 --- a/src/main.go +++ b/src/main.go @@ -14,28 +14,31 @@ import ( ) // Initialized in main +const configFilePath = "/etc/rhc/workers/rhc-worker-bash.yml" + var yggdDispatchSocketAddr string -var logFolder string -var logFileName string -var temporaryWorkerDirectory string -var shouldDoInsightsCoreGPGCheck string -var shouldVerifyYaml string +var config *Config // main is the entry point of the application. It initializes values from the environment, // sets up the logger, establishes a connection with the dispatcher, registers as a handler, // listens for incoming messages, and starts accepting connections as a Worker service. // Note: The function blocks and runs indefinitely until the server is stopped. func main() { - initializedOK, errorMsg := initializeEnvironment() - if errorMsg != "" && !initializedOK { - log.Fatal(errorMsg) + var yggSocketAddrExists bool // Has to be separately declared otherwise grpc.Dial doesn't work + yggdDispatchSocketAddr, yggSocketAddrExists = os.LookupEnv("YGG_SOCKET_ADDR") + if !yggSocketAddrExists { + log.Fatal("Missing YGG_SOCKET_ADDR environment variable") } - logFile := setupLogger(logFolder, logFileName) + config = loadConfigOrDefault(configFilePath) + log.Infoln("Configuration loaded: ", config) + + logFile := setupLogger(*config.LogDir, *config.LogDir) defer logFile.Close() // Dial the dispatcher on its well-known address. - conn, err := grpc.Dial(yggdDispatchSocketAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + conn, err := grpc.Dial( + yggdDispatchSocketAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatal(err) } @@ -47,7 +50,13 @@ func main() { defer cancel() // Register as a handler of the "rhc-worker-bash" type. - r, err := c.Register(ctx, &pb.RegistrationRequest{Handler: "rhc-worker-bash", Pid: int64(os.Getpid()), DetachedContent: true}) + r, err := c.Register( + ctx, + &pb.RegistrationRequest{ + Handler: "rhc-worker-bash", + Pid: int64(os.Getpid()), + DetachedContent: true, + }) if err != nil { log.Fatal(err) } diff --git a/src/runner.go b/src/runner.go index 3de76e6..11c3bbd 100644 --- a/src/runner.go +++ b/src/runner.go @@ -10,8 +10,8 @@ import ( "gopkg.in/yaml.v3" ) -// Received Yaml data has to match the expected yamlConfig structure -type yamlConfig struct { +// Received Yaml data has to match the expected signedYamlFile structure +type signedYamlFile struct { Vars struct { InsightsSignature string `yaml:"_insights_signature"` InsightsSignatureExclude string `yaml:"_insights_signature_exclude"` @@ -23,7 +23,7 @@ type yamlConfig struct { // Verify that no one tampered with yaml file func verifyYamlFile(yamlData []byte) bool { - if shouldVerifyYaml != "1" { + if !*config.VerifyYAML { log.Warnln("WARNING: Playbook verification disabled.") return true } @@ -38,7 +38,7 @@ func verifyYamlFile(yamlData []byte) bool { } env := os.Environ() - if shouldDoInsightsCoreGPGCheck == "0" { + if !*config.InsightsCoreGPGCheck { args = append(args, "--no-gpg") env = append(env, "BYPASS_GPG=True") } @@ -81,7 +81,7 @@ func processSignedScript(yamlFileContet []byte) string { log.Infoln("Signature of yaml file is valid") // Parse the YAML data into the yamlConfig struct - var yamlContent yamlConfig + var yamlContent signedYamlFile err := yaml.Unmarshal(yamlFileContet, &yamlContent) if err != nil { log.Errorln(err) @@ -117,7 +117,8 @@ func processSignedScript(yamlFileContet []byte) string { // Write the file contents to the temporary disk log.Infoln("Writing temporary bash script") - scriptFileName := writeFileToTemporaryDir([]byte(yamlContent.Vars.Content), temporaryWorkerDirectory) + scriptFileName := writeFileToTemporaryDir( + []byte(yamlContent.Vars.Content), *config.TemporaryWorkerDirectory) defer os.Remove(scriptFileName) // Execute the script diff --git a/src/runner_test.go b/src/runner_test.go index 0c58165..31c48c8 100644 --- a/src/runner_test.go +++ b/src/runner_test.go @@ -6,13 +6,19 @@ import ( ) func TestProcessSignedScript(t *testing.T) { - temporaryWorkerDirectory = "test-dir" + shouldVerifyYaml := false + shouldDoInsightsCoreGPGCheck := false + temporaryWorkerDirectory := "test-dir" + config = &Config{ + VerifyYAML: &shouldVerifyYaml, + TemporaryWorkerDirectory: &temporaryWorkerDirectory, + InsightsCoreGPGCheck: &shouldDoInsightsCoreGPGCheck, + } + defer os.RemoveAll(temporaryWorkerDirectory) // Test case 1: verification disabled, no yaml data supplied = empty output - shouldVerifyYaml = "0" yamlData := []byte{} - expectedResult := "" result := processSignedScript(yamlData) if result != expectedResult { @@ -38,8 +44,8 @@ vars: // FIXME: This is false success because verification fails on missing insighs-client // Test case 3: verification enabled, invalid signature = error msg returned - shouldVerifyYaml = "1" - shouldDoInsightsCoreGPGCheck = "0" + shouldVerifyYaml = true + shouldDoInsightsCoreGPGCheck = true expectedResult = "Signature of yaml file is invalid" result = processSignedScript(yamlData) if result != expectedResult { @@ -48,16 +54,22 @@ vars: } func TestVerifyYamlFile(t *testing.T) { - // Test case 1: shouldVerifyYaml is not "1" - shouldVerifyYaml = "0" + shouldVerifyYaml := false + shouldDoInsightsCoreGPGCheck := false + + config = &Config{ + VerifyYAML: &shouldVerifyYaml, + InsightsCoreGPGCheck: &shouldDoInsightsCoreGPGCheck, + } + // Test case 1: verification disabled expectedResult := true result := verifyYamlFile([]byte{}) if result != expectedResult { t.Errorf("Expected %v, but got %v", expectedResult, result) } - // Test case 2: shouldVerifyYaml is "1" and verification succeeds - shouldVerifyYaml = "1" + // Test case 2: verification enabled and verification succeeds + shouldVerifyYaml = true // FIXME: This should succedd but now verification fails on missing insighs-client // We also need valid signature expectedResult = false @@ -67,8 +79,8 @@ func TestVerifyYamlFile(t *testing.T) { } // FIXME: Valid test case but fails because of missing insights-client - // Test case 3: shouldVerifyYaml is "1" and verification fails - shouldVerifyYaml = "1" + // Test case 3: sverification is enabled and verification fails + // shouldVerifyYaml = true expectedResult = false result = verifyYamlFile([]byte("invalid-yaml")) // Replace with your YAML data if result != expectedResult { diff --git a/src/server.go b/src/server.go index 9bb5b07..017b1d9 100644 --- a/src/server.go +++ b/src/server.go @@ -65,7 +65,8 @@ func (s *jobServer) Send(ctx context.Context, d *pb.Data) (*pb.Receipt, error) { commandOutput := processSignedScript(d.GetContent()) // Dial the Dispatcher and call "Finish" - conn, err := grpc.Dial(yggdDispatchSocketAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + conn, err := grpc.Dial( + yggdDispatchSocketAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Error(err) } @@ -78,7 +79,8 @@ func (s *jobServer) Send(ctx context.Context, d *pb.Data) (*pb.Receipt, error) { // Create a data message to send back to the dispatcher. log.Infof("Creating payload for message %s", d.GetMessageId()) - data := createDataMessage(commandOutput, d.GetMetadata(), d.GetDirective(), d.GetMessageId()) + data := createDataMessage( + commandOutput, d.GetMetadata(), d.GetDirective(), d.GetMessageId()) // Call "Send" log.Infof("Sending message to %s", d.GetMessageId()) diff --git a/src/util.go b/src/util.go index 3aa3c3a..19f5176 100644 --- a/src/util.go +++ b/src/util.go @@ -10,31 +10,9 @@ import ( "os" "git.sr.ht/~spc/go-log" + "gopkg.in/yaml.v3" ) -// Calls os.LookupEnv for key, if not found then fallback value is returned -func getEnv(key, fallback string) string { - if value, ok := os.LookupEnv(key); ok { - return value - } - return fallback -} - -// Set initialization values from the environment variables -func initializeEnvironment() (bool, string) { - var yggSocketAddrExists bool // Has to be separately declared otherwise grpc.Dial doesn't work - yggdDispatchSocketAddr, yggSocketAddrExists = os.LookupEnv("YGG_SOCKET_ADDR") - if !yggSocketAddrExists { - return false, "Missing YGG_SOCKET_ADDR environment variable" - } - logFolder = getEnv("RHC_WORKER_LOG_FOLDER", "/var/log/rhc-worker-bash") - logFileName = getEnv("RHC_WORKER_LOG_FILENAME", "rhc-worker-bash.log") - temporaryWorkerDirectory = getEnv("RHC_WORKER_TMP_DIR", "/var/lib/rhc-worker-bash") - shouldDoInsightsCoreGPGCheck = getEnv("RHC_WORKER_GPG_CHECK", "1") - shouldVerifyYaml = getEnv("RHC_WORKER_VERIFY_YAML", "1") - return true, "" -} - // writeFileToTemporaryDir writes the provided data to a temporary file in the // designated temporary worker directory. It creates the directory if it doesn't exist. // The function returns the filename of the created temporary file. @@ -113,3 +91,80 @@ func constructMetadata(receivedMetadata map[string]string, contentType string) m } return ourMetadata } + +// Struc used fro worker global config +type Config struct { + Directive *string `yaml:"directive,omitempty"` + VerifyYAML *bool `yaml:"verify_yaml,omitempty"` + InsightsCoreGPGCheck *bool `yaml:"insights_core_gpg_check,omitempty"` + TemporaryWorkerDirectory *string `yaml:"temporary_worker_directory,omitempty"` + LogDir *string `yaml:"log_dir,omitempty"` + LogFileName *string `yaml:"log_filename,omitempty"` +} + +// Set default values for the Config struct +func setDefaultValues(config *Config) { + // Set default values for string and boolean fields if they are nil (not present in the YAML) + if config.Directive == nil { + defaultDirectiveValue := "rhc-worker-bash" + config.Directive = &defaultDirectiveValue + } + + if config.VerifyYAML == nil { + defaultVerifyYamlValue := true + config.VerifyYAML = &defaultVerifyYamlValue + } + + if config.InsightsCoreGPGCheck == nil { + defaultGpgCheckValue := true + config.InsightsCoreGPGCheck = &defaultGpgCheckValue + } + + if config.TemporaryWorkerDirectory == nil { + defaultTemporaryWorkerDirectoryValue := "/var/lib/rhc-worker-bash" + config.TemporaryWorkerDirectory = &defaultTemporaryWorkerDirectoryValue + } + + if config.LogDir == nil { + defaultLogFolder := "/var/log/rhc-worker-bash" + config.LogDir = &defaultLogFolder + } + + if config.LogFileName == nil { + defaultLogFilename := "rhc-worker-bash.log" + config.LogFileName = &defaultLogFilename + } +} + +// Load yaml config, if file doesn't exist or is invalid yaml then empty COnfig is returned +func loadYAMLConfig(filePath string) *Config { + var config Config + + data, err := os.ReadFile(filePath) + if err != nil { + log.Error(err) + } + + if err := yaml.Unmarshal(data, &config); err != nil { + log.Error(err) + } + + return &config +} + +// Load config from given filepath, if config doesn't exist then default config values are used +// Directive = rhc-worker-bash +// VerifyYAML = "1" +// InsightsCoreGPGCheck = "1" +func loadConfigOrDefault(filePath string) *Config { + config := &Config{} + _, err := os.Stat(filePath) + if err == nil { + // File exists, load configuration from YAML + config = loadYAMLConfig(filePath) + } + + // File doesn't exist, create a new Config with default values + setDefaultValues(config) + return config +} diff --git a/src/util_test.go b/src/util_test.go index 31ee536..29060c0 100644 --- a/src/util_test.go +++ b/src/util_test.go @@ -82,78 +82,107 @@ func TestWriteFileToTemporaryDir(t *testing.T) { } } -func TestGetEnv(t *testing.T) { - // Test case 1: When the environment variable exists - key := "MY_VARIABLE" - fallback := "default" - expected := "my-value" - os.Setenv(key, expected) - defer os.Unsetenv(key) - - result := getEnv(key, fallback) - if result != expected { - t.Errorf("Expected %s, but got %s", expected, result) +// Helper function to create a temporary YAML file with the given content and return its path. +func createTempYAMLFile(content string) (string, error) { + tempFile, err := os.CreateTemp("", "config_test_*.yaml") + if err != nil { + return "", err } + defer tempFile.Close() - // Test case 2: When the environment variable does not exist - key = "NON_EXISTENT_VARIABLE" - expected = fallback - - result = getEnv(key, fallback) - if result != expected { - t.Errorf("Expected %s, but got %s", expected, result) + if _, err := tempFile.WriteString(content); err != nil { + return "", err } -} -func TestInitializeEnvironment(t *testing.T) { - originalValue, existed := os.LookupEnv("YGG_SOCKET_ADDR") + return tempFile.Name(), nil +} - // Test case 1: default values with properly set YGG_SOCKET_ADDR - expectedYggdDispatchSocketAddr := "example.com" - os.Setenv("YGG_SOCKET_ADDR", expectedYggdDispatchSocketAddr) +func TestLoadConfigOrDefault(t *testing.T) { + expectedConfig := &Config{ + Directive: strPtr("rhc-worker-bash"), + VerifyYAML: boolPtr(true), + InsightsCoreGPGCheck: boolPtr(true), + LogFileName: strPtr("rhc-worker-bash.log"), + LogDir: strPtr("/var/log/rhc-worker-bash"), + TemporaryWorkerDirectory: strPtr("/var/lib/rhc-worker-bash"), + } + // Test case 1: No config present, defaults set + config := loadConfigOrDefault("foo-bar") + + if !compareConfigs(config, expectedConfig) { + t.Errorf("Loaded config does not match expected config") + } + + // Test case 2: Valid YAML file with all values present + yamlData := ` +directive: "rhc-worker-bash" +verify_yaml: true +verify_yaml_version_check: true +insights_core_gpg_check: true +log_dir: "/var/log/rhc-worker-bash" +log_filename: "rhc-worker-bash.log" +temporary_worker_directory: "/var/lib/rhc-worker-bash" +` + filePath, err := createTempYAMLFile(yamlData) + if err != nil { + t.Fatalf("Failed to create temporary YAML file: %v", err) + } + defer os.Remove(filePath) - ok, errorMsg := initializeEnvironment() + config = loadConfigOrDefault(filePath) - expectedValues := []struct { - name string - got string - expected string - }{ - {"yggdDispatchSocketAddr", yggdDispatchSocketAddr, expectedYggdDispatchSocketAddr}, - {"logFolder", logFolder, "/var/log/rhc-worker-bash"}, - {"logFileName", logFileName, "rhc-worker-bash.log"}, - {"temporaryWorkerDirectory", temporaryWorkerDirectory, "/var/lib/rhc-worker-bash"}, - {"shouldDoInsightsCoreGPGCheck", shouldDoInsightsCoreGPGCheck, "1"}, - {"shouldVerifyYaml", shouldVerifyYaml, "1"}, + if !compareConfigs(config, expectedConfig) { + t.Errorf("Loaded config does not match expected config") } - for _, value := range expectedValues { - if value.got != value.expected { - t.Errorf("Expected %s to be %s, but got %s", value.name, value.expected, value.got) - } + // Test case 3: Valid YAML file with missing values + yamlData = ` +directive: "rhc-worker-bash" +` + filePath, err = createTempYAMLFile(yamlData) + if err != nil { + t.Fatalf("Failed to create temporary YAML file: %v", err) } + defer os.Remove(filePath) + + config = loadConfigOrDefault(filePath) - if errorMsg != "" { - t.Errorf("Expected returned error message to be empty") + if !compareConfigs(config, expectedConfig) { + t.Errorf("Loaded config does not match expected config") } - if !ok { - t.Errorf("Expected returned status to be true") + + // Test case 4: Invalid YAML file - default config created + yamlData = ` +invalid_yaml_data +` + filePath, err = createTempYAMLFile(yamlData) + if err != nil { + t.Fatalf("Failed to create temporary YAML file: %v", err) } + defer os.Remove(filePath) - // Test case 2: default values with missing YGG_SOCKET_ADDR + config = loadConfigOrDefault(filePath) - os.Unsetenv("YGG_SOCKET_ADDR") - ok, errorMsg = initializeEnvironment() - if errorMsg == "" { - t.Errorf("Expected non-empty error message") - } - if ok { - t.Errorf("Expected returned status to be false") + if !compareConfigs(config, expectedConfig) { + t.Errorf("Loaded config does not match expected config") } +} + +// Helper function to compare two Config structs. +func compareConfigs(c1, c2 *Config) bool { + return *c1.Directive == *c2.Directive && + *c1.VerifyYAML == *c2.VerifyYAML && + *c1.InsightsCoreGPGCheck == *c2.InsightsCoreGPGCheck && + *c1.LogDir == *c2.LogDir && + *c1.LogFileName == *c2.LogFileName && + *c1.TemporaryWorkerDirectory == *c2.TemporaryWorkerDirectory +} + +// Helper functions for creating pointers to string and bool values. +func strPtr(s string) *string { + return &s +} - defer func() { - if existed { - os.Setenv("YGG_SOCKET_ADDR", originalValue) - } - }() +func boolPtr(b bool) *bool { + return &b }