diff --git a/cmd/agent/agent.go b/cmd/agent/agent.go
index e57447d25..89a35d850 100644
--- a/cmd/agent/agent.go
+++ b/cmd/agent/agent.go
@@ -23,6 +23,7 @@ import (
"net/http"
"os"
"runtime"
+ "strconv"
"strings"
"sync"
"time"
@@ -49,6 +50,7 @@ import (
)
func run(c *cli.Context) error {
+ agentIDConfigPath := c.String("agent-id-config-path")
hostname := c.String("hostname")
if len(hostname) == 0 {
hostname, _ = os.Hostname()
@@ -109,7 +111,7 @@ func run(c *cli.Context) error {
}
defer authConn.Close()
- agentID := int64(-1) // TODO: store agent id in a file
+ agentID := readAgentID(agentIDConfigPath)
agentToken := c.String("grpc-token")
authClient := agentRpc.NewAuthGrpcClient(authConn, agentToken, agentID)
authInterceptor, err := agentRpc.NewAuthInterceptor(authClient, 30*time.Minute)
@@ -178,6 +180,8 @@ func run(c *cli.Context) error {
return err
}
+ writeAgentID(agentID, agentIDConfigPath)
+
labels := map[string]string{
"hostname": hostname,
"platform": platform,
@@ -280,3 +284,33 @@ 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 1e645e68d..ded229ed6 100644
--- a/cmd/agent/agent_test.go
+++ b/cmd/agent/agent_test.go
@@ -15,6 +15,8 @@
package main
import (
+ "fmt"
+ "os"
"testing"
"github.com/stretchr/testify/assert"
@@ -73,3 +75,91 @@ 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/flags.go b/cmd/agent/flags.go
index 8a191b791..5b6b09c52 100644
--- a/cmd/agent/flags.go
+++ b/cmd/agent/flags.go
@@ -67,6 +67,12 @@ var flags = []cli.Flag{
Name: "hostname",
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",
+ },
&cli.StringSliceFlag{
EnvVars: []string{"WOODPECKER_FILTER_LABELS"},
Name: "filter",
diff --git a/docker/Dockerfile.agent.alpine.multiarch b/docker/Dockerfile.agent.alpine.multiarch
index 4e88c0604..d4fba3e66 100644
--- a/docker/Dockerfile.agent.alpine.multiarch
+++ b/docker/Dockerfile.agent.alpine.multiarch
@@ -13,6 +13,7 @@ ENV GODEBUG=netdns=go
EXPOSE 3000
COPY --from=build /src/dist/woodpecker-agent /bin/
+RUN mkdir -p /etc/woodpecker
HEALTHCHECK CMD ["/bin/woodpecker-agent", "ping"]
ENTRYPOINT ["/bin/woodpecker-agent"]
diff --git a/docker/Dockerfile.agent.multiarch b/docker/Dockerfile.agent.multiarch
index 93fb8b4e6..6bcf5b766 100644
--- a/docker/Dockerfile.agent.multiarch
+++ b/docker/Dockerfile.agent.multiarch
@@ -6,6 +6,7 @@ ARG TARGETOS TARGETARCH
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg \
make build-agent
+RUN mkdir -p /etc/woodpecker
FROM scratch
ENV GODEBUG=netdns=go
@@ -15,6 +16,7 @@ EXPOSE 3000
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
# copy agent binary
COPY --from=build /src/dist/woodpecker-agent /bin/
+COPY --from=build /etc/woodpecker /etc
HEALTHCHECK CMD ["/bin/woodpecker-agent", "ping"]
ENTRYPOINT ["/bin/woodpecker-agent"]
diff --git a/docs/docs/30-administration/15-agent-config.md b/docs/docs/30-administration/15-agent-config.md
index 6d1394098..ed6526663 100644
--- a/docs/docs/30-administration/15-agent-config.md
+++ b/docs/docs/30-administration/15-agent-config.md
@@ -58,7 +58,7 @@ A shared secret used by server and agents to authenticate communication. A secre
### `WOODPECKER_AGENT_SECRET_FILE`
> Default: empty
-Read the value for `WOODPECKER_AGENT_SECRET` from the specified filepath
+Read the value for `WOODPECKER_AGENT_SECRET` from the specified filepath, e.g. `/etc/woodpecker/agent-secret.conf`
### `WOODPECKER_LOG_LEVEL`
> Default: empty
@@ -80,6 +80,11 @@ Disable colored debug output.
Configures the agent hostname.
+### `WOODPECKER_AGENT_ID_FILE`
+> Default: `/etc/woodpecker/agent-id.conf`
+
+Configures the path of the agent-id.conf file.
+
### `WOODPECKER_MAX_WORKFLOWS`
> Default: `1`
diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json
index f4dfb3611..b8f421204 100644
--- a/web/src/assets/locales/en.json
+++ b/web/src/assets/locales/en.json
@@ -342,6 +342,7 @@
"agents": "Agents",
"desc": "Agents registered for this server",
"none": "There are no agents yet.",
+ "id": "ID",
"add": "Add agent",
"save": "Save agent",
"show": "Show agents",
diff --git a/web/src/components/admin/settings/AdminAgentsTab.vue b/web/src/components/admin/settings/AdminAgentsTab.vue
index 6492bd0f0..8598173b2 100644
--- a/web/src/components/admin/settings/AdminAgentsTab.vue
+++ b/web/src/components/admin/settings/AdminAgentsTab.vue
@@ -66,6 +66,10 @@
+
+
+
+