Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to create config for rhc-worker-bash #50

Merged
merged 1 commit into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 36 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
31 changes: 20 additions & 11 deletions src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down
13 changes: 7 additions & 6 deletions src/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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
}
Expand All @@ -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")
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
34 changes: 23 additions & 11 deletions src/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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 {
Expand Down
6 changes: 4 additions & 2 deletions src/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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())
Expand Down
101 changes: 78 additions & 23 deletions src/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Loading