Skip to content

Commit 37b26c0

Browse files
authored
feat(agent): lts tag and a flag for agent image tag (#177)
1 parent 9f3103a commit 37b26c0

22 files changed

Lines changed: 164 additions & 29 deletions

File tree

INSTALL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export KUBESOLO_PATH="/var/lib/kubesolo" # Installation path
9898
export KUBESOLO_PORTAINER_EDGE_ID="your-id" # Portainer Edge ID
9999
export KUBESOLO_PORTAINER_EDGE_KEY="your-key" # Portainer Edge Key
100100
export KUBESOLO_PORTAINER_EDGE_ASYNC="false" # Async mode
101+
export KUBESOLO_PORTAINER_EDGE_IMAGE="docker.io/portainer/agent:lts" # Edge Agent image
101102
export KUBESOLO_LOCAL_STORAGE="false" # Enable local storage
102103
export KUBESOLO_DEBUG="false" # Debug logging
103104
export KUBESOLO_PPROF_SERVER="false" # Enable pprof

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ KubeSolo supports the following command-line flags:
159159
| `--portainer-edge-id` | `KUBESOLO_PORTAINER_EDGE_ID` | Portainer Edge ID | `""` |
160160
| `--portainer-edge-key` | `KUBESOLO_PORTAINER_EDGE_KEY` | Portainer Edge Key | `""` |
161161
| `--portainer-edge-async` | `KUBESOLO_PORTAINER_EDGE_ASYNC` | Enable Portainer Edge Async Mode | `false` |
162+
| `--portainer-edge-image` | `KUBESOLO_PORTAINER_EDGE_IMAGE` | Image deployed for the Portainer Edge Agent, including the tag. Any image other than the default is pulled from the registry rather than loaded from the embedded image | `docker.io/portainer/agent:lts` |
162163
| `--load-balancer` | `KUBESOLO_LOAD_BALANCER` | Enable load balancer. With this enabled, kubesolo will update a newly deployed service with the load balancer type so that the EXTERNAL-IP is set to the node IP | `true` |
163164
| `--local-storage` | `KUBESOLO_LOCAL_STORAGE` | Enable local storage | `true` |
164165
| `--local-storage-shared-path` | `KUBESOLO_LOCAL_STORAGE_SHARED_PATH` | Path to the shared file system for the local storage | `""` |

build/download-deps.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ ARCH="amd64"
88
CONTAINERD_VERSION="2.2.5"
99
CRUN_VERSION="1.26"
1010
CNI_VERSION="v1.9.0"
11-
PORTAINER_AGENT_VERSION="2.39.4"
11+
PORTAINER_AGENT_VERSION="lts"
1212
COREDNS_VERSION="1.14.4"
1313
LOCAL_PATH_PROVISIONER_VERSION="v0.0.36"
1414
PAUSE_IMAGE_VERSION="3.10"

cmd/kubesolo/main.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"context"
5+
"fmt"
56
"os"
67
"os/signal"
78
"path/filepath"
@@ -11,6 +12,7 @@ import (
1112
"syscall"
1213

1314
"github.com/alecthomas/kingpin/v2"
15+
"github.com/distribution/reference"
1416
"github.com/portainer/kubesolo/internal/config/flags"
1517
"github.com/portainer/kubesolo/internal/core/embedded"
1618
"github.com/portainer/kubesolo/internal/core/pki"
@@ -47,6 +49,7 @@ type kubesolo struct {
4749
portainerEdgeID string
4850
portainerEdgeKey string
4951
portainerEdgeAsync bool
52+
portainerEdgeImage string
5053
loadBalancer bool
5154
localStorage bool
5255
localStorageSharedPath string
@@ -78,6 +81,11 @@ func service() (*kubesolo, error) {
7881
log.Fatal().Str("component", "kubesolo").Msg("--d2k requires --load-balancer: the d2k Service endpoint is populated by the LoadBalancer webhook")
7982
}
8083

84+
portainerEdgeImage, err := normaliseImageRef(*flags.PortainerEdgeImage)
85+
if err != nil {
86+
return nil, err
87+
}
88+
8189
return &kubesolo{
8290
hostName: system.GetHostname(),
8391
extraSANs: *flags.APIServerExtraSANs,
@@ -86,6 +94,7 @@ func service() (*kubesolo, error) {
8694
portainerEdgeID: *flags.PortainerEdgeID,
8795
portainerEdgeKey: *flags.PortainerEdgeKey,
8896
portainerEdgeAsync: *flags.PortainerEdgeAsync,
97+
portainerEdgeImage: portainerEdgeImage,
8998
loadBalancer: *flags.LoadBalancer,
9099
localStorage: *flags.LocalStorage,
91100
localStorageSharedPath: *flags.LocalStorageSharedPath,
@@ -96,6 +105,19 @@ func service() (*kubesolo, error) {
96105
}, nil
97106
}
98107

108+
// normaliseImageRef expands a short image reference such as portainerci/agent:develop
109+
// into a fully qualified one (docker.io/portainerci/agent:develop). The containerd
110+
// client, unlike the Docker CLI, applies no Docker Hub defaults and would otherwise
111+
// treat the first component as a registry host and fail to resolve it.
112+
func normaliseImageRef(image string) (string, error) {
113+
named, err := reference.ParseNormalizedNamed(image)
114+
if err != nil {
115+
return "", fmt.Errorf("invalid image reference %q: %v", image, err)
116+
}
117+
118+
return reference.TagNameOnly(named).String(), nil
119+
}
120+
99121
// main is the entry point for the kubesolo application
100122
// it parses the command line arguments and creates a new kubesolo application
101123
// it then bootstraps the application and runs it
@@ -276,6 +298,7 @@ func (s *kubesolo) run() {
276298
if s.portainerEdgeID != "" && s.portainerEdgeKey != "" {
277299
log.Info().Str("component", "kubesolo").Msg("deploying portainer edge agent...")
278300
if err := portainer.DeployEdgeAgent(s.embedded.AdminKubeconfigFile, types.EdgeAgentConfig{
301+
Image: s.portainerEdgeImage,
279302
EdgeID: s.portainerEdgeID,
280303
EdgeKey: s.portainerEdgeKey,
281304
EdgeAsync: s.portainerEdgeAsync,
@@ -531,7 +554,7 @@ func (s *kubesolo) bootstrap() {
531554
WebhookDir: filepath.Join(basePath, types.KubesoloWebhookDir),
532555

533556
// Image paths
534-
PortainerAgentImageFile: filepath.Join(basePath, types.DefaultContainerdDir, "images", "portainer-agent.tar.gz"),
557+
PortainerEdgeImageFile: filepath.Join(basePath, types.DefaultContainerdDir, "images", "portainer-agent.tar.gz"),
535558
CorednsImageFile: filepath.Join(basePath, types.DefaultContainerdDir, "images", "coredns.tar.gz"),
536559
SandboxImageFile: filepath.Join(basePath, types.DefaultContainerdDir, "images", "pause.tar.gz"),
537560
LocalPathProvisionerImageFile: filepath.Join(basePath, types.DefaultContainerdDir, "images", "local-path-provisioner.tar.gz"),
@@ -543,7 +566,8 @@ func (s *kubesolo) bootstrap() {
543566
LocalPathStorageDir: filepath.Join(basePath, types.DefaultLocalPathStorageDir),
544567

545568
// Portainer Edge
546-
IsPortainerEdge: s.portainerEdgeID != "" && s.portainerEdgeKey != "",
569+
IsPortainerEdge: s.portainerEdgeID != "" && s.portainerEdgeKey != "",
570+
PortainerEdgeImage: s.portainerEdgeImage,
547571

548572
// Container Mode
549573
ContainerMode: containerMode,

cmd/kubesolo/main_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package main
2+
3+
import "testing"
4+
5+
func TestNormaliseImageRef(t *testing.T) {
6+
tests := []struct {
7+
in string
8+
want string
9+
}{
10+
{"docker.io/portainer/agent:lts", "docker.io/portainer/agent:lts"},
11+
{"portainer/agent:lts", "docker.io/portainer/agent:lts"},
12+
{"portainerci/agent:develop", "docker.io/portainerci/agent:develop"},
13+
{"portainerci/agent", "docker.io/portainerci/agent:latest"},
14+
{"ghcr.io/portainer/agent:1.0", "ghcr.io/portainer/agent:1.0"},
15+
{"localhost:5000/agent:dev", "localhost:5000/agent:dev"},
16+
}
17+
18+
for _, tt := range tests {
19+
got, err := normaliseImageRef(tt.in)
20+
if err != nil {
21+
t.Errorf("normaliseImageRef(%q) returned error: %v", tt.in, err)
22+
continue
23+
}
24+
if got != tt.want {
25+
t.Errorf("normaliseImageRef(%q) = %q, want %q", tt.in, got, tt.want)
26+
}
27+
}
28+
29+
if _, err := normaliseImageRef("NOT A REF"); err == nil {
30+
t.Errorf("normaliseImageRef should reject an invalid reference")
31+
}
32+
}

docs/installation/flags.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,27 @@ curl -sfL https://get.kubesolo.io | \
116116

117117
---
118118

119+
### --portainer-edge-image
120+
121+
Full image reference deployed for the Portainer Edge Agent, including the tag. Accepts any registry, repository, and tag. Only the default image is loaded from the bundled image; any other reference is pulled from the registry, so the node needs access to it. If the registry cannot be reached at startup, KubeSolo logs a warning and continues — the kubelet retries the pull when the agent pod starts.
122+
123+
Short references are expanded the way Docker expands them, so `portainerci/agent:develop` becomes `docker.io/portainerci/agent:develop` and an omitted tag defaults to `latest`. Use a full host prefix for other registries, e.g. `ghcr.io/portainer/agent:2.34.0`.
124+
125+
Note that the deployment is only created once — on reboot it is restored from the database. Changing this flag on an existing installation does not update an already deployed agent.
126+
127+
| Flag | Env var | Default |
128+
|---|---|---|
129+
| `--portainer-edge-image=IMAGE` | `KUBESOLO_PORTAINER_EDGE_IMAGE` | `docker.io/portainer/agent:lts` |
130+
131+
```bash
132+
curl -sfL https://get.kubesolo.io | \
133+
KUBESOLO_PORTAINER_EDGE_ID=<your-edge-id> \
134+
KUBESOLO_PORTAINER_EDGE_KEY=<your-edge-key> \
135+
sudo -E sh -s -- --portainer-edge-image=docker.io/portainer/agent:sts
136+
```
137+
138+
---
139+
119140
### --local-storage
120141

121142
Enable the [Local Path Provisioner](https://github.com/rancher/local-path-provisioner), which creates a `local-path` StorageClass backed by host-local directories. Workloads that request persistent volumes will have them provisioned automatically under the KubeSolo data path.

docs/installation/kubesoloctl.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ Common flags:
104104
| `--container-ports` | `KUBESOLO_CONTAINER_PORTS` | _(none)_ | Workload host ports to publish (container mode) |
105105
| `--d2k` | `KUBESOLO_D2K` | `false` | Enable the Docker-compatible API translator |
106106
| `--local-storage` | `KUBESOLO_LOCAL_STORAGE` | `false` | Enable the local-path storage provisioner |
107+
| `--portainer-edge-image` | `KUBESOLO_PORTAINER_EDGE_IMAGE` | `docker.io/portainer/agent:lts` | Image deployed for the Portainer edge agent |
107108
| `--offline-install` | `KUBESOLO_OFFLINE_INSTALL` | _(none)_ | Install from a local tarball/binary instead of downloading |
108109
| `--proxy` | `KUBESOLO_PROXY` | _(none)_ | HTTP/HTTPS proxy injected into the service environment |
109110

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/containerd/containerd/v2 v2.2.3
88
github.com/containerd/errdefs v1.0.0
99
github.com/containerd/fuse-overlayfs-snapshotter/v2 v2.1.7
10+
github.com/distribution/reference v0.6.0
1011
github.com/docker/docker v28.5.2+incompatible
1112
github.com/docker/go-connections v0.5.0
1213
github.com/k3s-io/kine v0.15.0
@@ -77,7 +78,6 @@ require (
7778
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
7879
github.com/cyphar/filepath-securejoin v0.6.0 // indirect
7980
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
80-
github.com/distribution/reference v0.6.0 // indirect
8181
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
8282
github.com/docker/go-metrics v0.0.1 // indirect
8383
github.com/docker/go-units v0.5.0 // indirect

install.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,6 +1082,7 @@ APISERVER_EXTRA_SANS="${KUBESOLO_APISERVER_EXTRA_SANS:-}"
10821082
PORTAINER_EDGE_ID="${KUBESOLO_PORTAINER_EDGE_ID:-}"
10831083
PORTAINER_EDGE_KEY="${KUBESOLO_PORTAINER_EDGE_KEY:-}"
10841084
PORTAINER_EDGE_ASYNC="${KUBESOLO_PORTAINER_EDGE_ASYNC:-false}"
1085+
PORTAINER_EDGE_IMAGE="${KUBESOLO_PORTAINER_EDGE_IMAGE:-}"
10851086
LOAD_BALANCER="${KUBESOLO_LOAD_BALANCER:-true}"
10861087
LOCAL_STORAGE="${KUBESOLO_LOCAL_STORAGE:-true}"
10871088
LOCAL_STORAGE_SHARED_PATH="${KUBESOLO_LOCAL_STORAGE_SHARED_PATH:-}"
@@ -1120,6 +1121,9 @@ for arg in "$@"; do
11201121
--portainer-edge-async=*)
11211122
PORTAINER_EDGE_ASYNC="${arg#*=}"
11221123
;;
1124+
--portainer-edge-image=*)
1125+
PORTAINER_EDGE_IMAGE="${arg#*=}"
1126+
;;
11231127
--local-storage=*)
11241128
LOCAL_STORAGE="${arg#*=}"
11251129
;;
@@ -1165,6 +1169,7 @@ for arg in "$@"; do
11651169
echo " --portainer-edge-id=ID Set Portainer Edge ID"
11661170
echo " --portainer-edge-key=KEY Set Portainer Edge Key"
11671171
echo " --portainer-edge-async=true|false Enable Portainer Edge Async (default: $PORTAINER_EDGE_ASYNC)"
1172+
echo " --portainer-edge-image=IMAGE Set the Portainer Edge Agent image (default: docker.io/portainer/agent:lts)"
11681173
echo " --local-storage=true|false Enable local storage (default: $LOCAL_STORAGE)"
11691174
echo " --d2k=true|false Embed d2k Docker-to-Kubernetes API translator (default: $D2K)"
11701175
echo " --d2k-namespace=NAMESPACE Namespace into which d2k is deployed (default: $D2K_NAMESPACE)"
@@ -1304,6 +1309,10 @@ if [ "$PORTAINER_EDGE_ASYNC" = "true" ]; then
13041309
CMD_ARGS="$CMD_ARGS --portainer-edge-async=true"
13051310
fi
13061311

1312+
if [ -n "$PORTAINER_EDGE_IMAGE" ]; then
1313+
CMD_ARGS="$CMD_ARGS --portainer-edge-image=$PORTAINER_EDGE_IMAGE"
1314+
fi
1315+
13071316
if [ "$LOAD_BALANCER" = "false" ]; then
13081317
CMD_ARGS="$CMD_ARGS --load-balancer=false"
13091318
fi

internal/cli/cmd_install.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ func addInstallFlags(cmd *cobra.Command, cfg *config.Config) {
6161
envBool("KUBESOLO_PORTAINER_EDGE_ASYNC", false),
6262
"Enable async mode for the Portainer edge agent")
6363

64+
f.StringVar(&cfg.PortainerEdgeImage, "portainer-edge-image",
65+
os.Getenv("KUBESOLO_PORTAINER_EDGE_IMAGE"),
66+
"Image deployed for the Portainer edge agent, including the tag\n"+
67+
"(default: docker.io/portainer/agent:lts). Any other image is pulled from the registry")
68+
6469
f.BoolVar(&cfg.LocalStorage, "local-storage",
6570
envBool("KUBESOLO_LOCAL_STORAGE", false),
6671
"Enable the local-path storage provisioner")

0 commit comments

Comments
 (0)