diff --git a/cmd/agent/agent.go b/cmd/agent/agent.go index 89a35d850..96f1b23be 100644 --- a/cmd/agent/agent.go +++ b/cmd/agent/agent.go @@ -23,7 +23,6 @@ import ( "net/http" "os" "runtime" - "strconv" "strings" "sync" "time" @@ -50,7 +49,7 @@ import ( ) func run(c *cli.Context) error { - agentIDConfigPath := c.String("agent-id-config-path") + agentConfigPath := c.String("agent-config") hostname := c.String("hostname") if len(hostname) == 0 { hostname, _ = os.Hostname() @@ -111,9 +110,15 @@ func run(c *cli.Context) error { } defer authConn.Close() - agentID := readAgentID(agentIDConfigPath) + agentConfig := readAgentConfig(agentConfigPath) + + // deprecated + if agentConfig.AgentID == defaultAgentIDValue { + agentConfig.AgentID = readAgentID(c.String("agent-id-config-path")) + } + agentToken := c.String("grpc-token") - authClient := agentRpc.NewAuthGrpcClient(authConn, agentToken, agentID) + authClient := agentRpc.NewAuthGrpcClient(authConn, agentToken, agentConfig.AgentID) authInterceptor, err := agentRpc.NewAuthInterceptor(authClient, 30*time.Minute) if err != nil { return err @@ -175,12 +180,12 @@ func run(c *cli.Context) error { return err } - agentID, err = client.RegisterAgent(ctx, platform, engine.Name(), version.String(), parallel) + agentConfig.AgentID, err = client.RegisterAgent(ctx, platform, engine.Name(), version.String(), parallel) if err != nil { return err } - writeAgentID(agentID, agentIDConfigPath) + writeAgentConfig(agentConfig, agentConfigPath) labels := map[string]string{ "hostname": hostname, @@ -197,7 +202,7 @@ func run(c *cli.Context) error { Labels: labels, } - log.Debug().Msgf("Agent registered with ID %d", agentID) + log.Debug().Msgf("Agent registered with ID %d", agentConfig.AgentID) go func() { for { @@ -284,33 +289,3 @@ func stringSliceAddToMap(sl []string, m map[string]string) error { } return nil } - -func readAgentID(agentIDConfigPath string) int64 { - const defaultAgentIDValue = int64(-1) - - rawAgentID, fileErr := os.ReadFile(agentIDConfigPath) - if fileErr != nil { - log.Debug().Err(fileErr).Msgf("could not open agent-id config file from %s", agentIDConfigPath) - return defaultAgentIDValue - } - - strAgentID := strings.TrimSpace(string(rawAgentID)) - agentID, parseErr := strconv.ParseInt(strAgentID, 10, 64) - if parseErr != nil { - log.Warn().Err(parseErr).Msg("could not parse agent-id config file content to int64") - return defaultAgentIDValue - } - - return agentID -} - -func writeAgentID(agentID int64, agentIDConfigPath string) { - currentAgentID := readAgentID(agentIDConfigPath) - - if currentAgentID != agentID { - err := os.WriteFile(agentIDConfigPath, []byte(strconv.FormatInt(agentID, 10)+"\n"), 0o644) - if err != nil { - log.Warn().Err(err).Msgf("could not write agent-id config file to %s", agentIDConfigPath) - } - } -} diff --git a/cmd/agent/agent_test.go b/cmd/agent/agent_test.go index ded229ed6..1e645e68d 100644 --- a/cmd/agent/agent_test.go +++ b/cmd/agent/agent_test.go @@ -15,8 +15,6 @@ package main import ( - "fmt" - "os" "testing" "github.com/stretchr/testify/assert" @@ -75,91 +73,3 @@ func TestStringSliceAddToMap(t *testing.T) { }) } } - -func TestReadAgentIDFileNotExists(t *testing.T) { - assert.EqualValues(t, -1, readAgentID("foobar.conf")) -} - -func TestReadAgentIDFileExists(t *testing.T) { - parameters := []struct { - input string - expected int64 - }{ - {"42", 42}, - {"42\n", 42}, - {" \t42\t\r\t", 42}, - {"0", 0}, - {"-1", -1}, - {"foo", -1}, - {"1f", -1}, - {"", -1}, - {"-42", -42}, - } - - for i := range parameters { - t.Run(fmt.Sprintf("Testing [%v]", i), func(t *testing.T) { - tmpF, errTmpF := os.CreateTemp("", "tmp_") - if !assert.NoError(t, errTmpF) { - t.FailNow() - } - - errWrite := os.WriteFile(tmpF.Name(), []byte(parameters[i].input), 0o644) - if !assert.NoError(t, errWrite) { - t.FailNow() - } - - actual := readAgentID(tmpF.Name()) - assert.EqualValues(t, parameters[i].expected, actual) - }) - } -} - -func TestWriteAgentIDFileNotExists(t *testing.T) { - tmpF, errTmpF := os.CreateTemp("", "tmp_") - if !assert.NoError(t, errTmpF) { - t.FailNow() - } - - writeAgentID(42, tmpF.Name()) - actual, errRead := os.ReadFile(tmpF.Name()) - if !assert.NoError(t, errRead) { - t.FailNow() - } - assert.EqualValues(t, "42\n", actual) -} - -func TestWriteAgentIDFileExists(t *testing.T) { - parameters := []struct { - fileInput string - writeInput int64 - expected string - }{ - {"", 42, "42\n"}, - {"\n", 42, "42\n"}, - {"41\n", 42, "42\n"}, - {"0", 42, "42\n"}, - {"-1", 42, "42\n"}, - {"foƶbar", 42, "42\n"}, - } - - for i := range parameters { - t.Run(fmt.Sprintf("Testing [%v]", i), func(t *testing.T) { - tmpF, errTmpF := os.CreateTemp("", "tmp_") - if !assert.NoError(t, errTmpF) { - t.FailNow() - } - - errWrite := os.WriteFile(tmpF.Name(), []byte(parameters[i].fileInput), 0o644) - if !assert.NoError(t, errWrite) { - t.FailNow() - } - - writeAgentID(parameters[i].writeInput, tmpF.Name()) - actual, errRead := os.ReadFile(tmpF.Name()) - if !assert.NoError(t, errRead) { - t.FailNow() - } - assert.EqualValues(t, parameters[i].expected, actual) - }) - } -} diff --git a/cmd/agent/config.go b/cmd/agent/config.go new file mode 100644 index 000000000..e477099fa --- /dev/null +++ b/cmd/agent/config.go @@ -0,0 +1,94 @@ +// Copyright 2022 Woodpecker Authors +// Copyright 2019 Laszlo Fogas +// +// 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 main + +import ( + "bytes" + "encoding/json" + "os" + "strconv" + "strings" + + "github.com/rs/zerolog/log" +) + +type AgentConfig struct { + AgentID int64 `json:"agent_id"` +} + +const defaultAgentIDValue = int64(-1) + +func readAgentConfig(agentConfigPath string) AgentConfig { + conf := AgentConfig{ + AgentID: defaultAgentIDValue, + } + + rawAgentConf, err := os.ReadFile(agentConfigPath) + if err != nil { + if !os.IsNotExist(err) { + log.Info().Msgf("no agent config found at '%s', start with defaults", agentConfigPath) + } else { + log.Error().Err(err).Msgf("could not open agent config at '%s'", agentConfigPath) + } + return conf + } + if strings.TrimSpace(string(rawAgentConf)) == "" { + return conf + } + + if err := json.Unmarshal(rawAgentConf, &conf); err != nil { + log.Error().Err(err).Msg("could not parse agent config") + } + return conf +} + +func writeAgentConfig(conf AgentConfig, agentConfigPath string) { + rawAgentConf, err := json.Marshal(conf) + if err != nil { + log.Error().Err(err).Msg("could not marshal agent config") + return + } + + // get old config + oldRawAgentConf, _ := os.ReadFile(agentConfigPath) + + // if config differ write to disk + if bytes.Equal(rawAgentConf, oldRawAgentConf) { + if err := os.WriteFile(agentConfigPath, rawAgentConf, 0o644); err != nil { + log.Error().Err(err).Msgf("could not persist agent config at '%s'", agentConfigPath) + } + } +} + +// deprecated +func readAgentID(agentIDConfigPath string) int64 { + const defaultAgentIDValue = int64(-1) + + rawAgentID, fileErr := os.ReadFile(agentIDConfigPath) + if fileErr != nil { + log.Debug().Err(fileErr).Msgf("could not open agent-id config file from %s", agentIDConfigPath) + return defaultAgentIDValue + } + + strAgentID := strings.TrimSpace(string(rawAgentID)) + agentID, parseErr := strconv.ParseInt(strAgentID, 10, 64) + if parseErr != nil { + log.Warn().Err(parseErr).Msg("could not parse agent-id config file content to int64") + return defaultAgentIDValue + } + + return agentID +} diff --git a/cmd/agent/config_test.go b/cmd/agent/config_test.go new file mode 100644 index 000000000..a5248f722 --- /dev/null +++ b/cmd/agent/config_test.go @@ -0,0 +1,61 @@ +// Copyright 2023 Woodpecker Authors +// +// 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 main + +import ( + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReadAgentIDFileNotExists(t *testing.T) { + assert.EqualValues(t, -1, readAgentID("foobar.conf")) +} + +func TestReadAgentIDFileExists(t *testing.T) { + parameters := []struct { + input string + expected int64 + }{ + {"42", 42}, + {"42\n", 42}, + {" \t42\t\r\t", 42}, + {"0", 0}, + {"-1", -1}, + {"foo", -1}, + {"1f", -1}, + {"", -1}, + {"-42", -42}, + } + + for i := range parameters { + t.Run(fmt.Sprintf("Testing [%v]", i), func(t *testing.T) { + tmpF, errTmpF := os.CreateTemp("", "tmp_") + if !assert.NoError(t, errTmpF) { + t.FailNow() + } + + errWrite := os.WriteFile(tmpF.Name(), []byte(parameters[i].input), 0o644) + if !assert.NoError(t, errWrite) { + t.FailNow() + } + + actual := readAgentID(tmpF.Name()) + assert.EqualValues(t, parameters[i].expected, actual) + }) + } +} diff --git a/cmd/agent/flags.go b/cmd/agent/flags.go index 5b6b09c52..5e6125b62 100644 --- a/cmd/agent/flags.go +++ b/cmd/agent/flags.go @@ -68,10 +68,10 @@ var flags = []cli.Flag{ Usage: "agent hostname", }, &cli.StringFlag{ - EnvVars: []string{"WOODPECKER_AGENT_ID_FILE"}, - Name: "agent-id-config-path", - Usage: "agent-id config file path", - Value: "/etc/woodpecker/agent-id.conf", + EnvVars: []string{"WOODPECKER_AGENT_CONFIG_FILE"}, + Name: "agent-config", + Usage: "agent config file path", + Value: "/etc/woodpecker/agent.conf", }, &cli.StringSliceFlag{ EnvVars: []string{"WOODPECKER_FILTER_LABELS"}, @@ -208,4 +208,13 @@ var flags = []cli.Flag{ Usage: "duration to wait before retrying to connect to the server", Value: time.Second * 2, }, + + // DEPRECATED + &cli.StringFlag{ + EnvVars: []string{"WOODPECKER_AGENT_ID_FILE"}, + Name: "agent-id-config-path", + Usage: "agent-id config file path", + Value: "/etc/woodpecker/agent-id.conf", + Hidden: true, + }, } diff --git a/docs/docs/30-administration/00-setup.md b/docs/docs/30-administration/00-setup.md index 689227858..e4cfcc065 100644 --- a/docs/docs/30-administration/00-setup.md +++ b/docs/docs/30-administration/00-setup.md @@ -70,6 +70,7 @@ services: depends_on: - woodpecker-server volumes: + - woodpecker-agent-config:/etc/woodpecker - /var/run/docker.sock:/var/run/docker.sock environment: - WOODPECKER_SERVER=woodpecker-server:9000 diff --git a/docs/docs/30-administration/15-agent-config.md b/docs/docs/30-administration/15-agent-config.md index ca3f536b4..94fb03f67 100644 --- a/docs/docs/30-administration/15-agent-config.md +++ b/docs/docs/30-administration/15-agent-config.md @@ -8,10 +8,12 @@ version: '3' services: woodpecker-agent: - [...] - environment: -+ - WOODPECKER_SERVER=localhost:9000 -+ - WOODPECKER_AGENT_SECRET="your-shared-secret-goes-here" + [...] + volumes: + - woodpecker-agent-config:/etc/woodpecker + environment: ++ - WOODPECKER_SERVER=localhost:9000 ++ - WOODPECKER_AGENT_SECRET="your-shared-secret-goes-here" ``` The following are automatically set and can be overridden: @@ -49,7 +51,7 @@ In that case registration process would be as follows: 1. First time Agent communicates with Server using system token; 2. Server registers Agent in DB, generates ID and sends this ID back to Agent; -3. Agent stores ID in a file configured by `WOODPECKER_AGENT_ID_FILE`. +3. Agent stores ID in a file configured by `WOODPECKER_AGENT_CONFIG_FILE`. At the following startups Agent uses system token **and** ID. @@ -113,10 +115,10 @@ Disable colored debug output. Configures the agent hostname. -### `WOODPECKER_AGENT_ID_FILE` -> Default: `/etc/woodpecker/agent-id.conf` +### `WOODPECKER_AGENT_CONFIG_FILE` +> Default: `/etc/woodpecker/agent.conf` -Configures the path of the agent-id.conf file. +Configures the path of the agent config file. ### `WOODPECKER_MAX_WORKFLOWS` > Default: `1`