Skip to content
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
19 changes: 10 additions & 9 deletions .github/workflows/zos-update-worker-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,25 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Install GO
uses: actions/setup-go@v3
with:
go-version: 1.19
uses: actions/setup-go@v5
with:
go-version: "1.23"

- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v7
with:
version: v2.1.6
args: --timeout 3m --verbose
working-directory: tools/zos-update-worker
working-directory: tools/zos-update-worker

- name: staticcheck
uses: dominikh/staticcheck-action@v1.3.0
uses: dominikh/staticcheck-action@v1.4.0
with:
version: "2022.1.3"
working-directory: tools/zos-update-worker
version: "2025.1.1"
working-directory: tools/zos-update-worker
env:
GO111MODULE: on

Expand Down
29 changes: 17 additions & 12 deletions tools/zos-update-worker/internal/update_worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ func checkNetwork(network Network) error {
return nil
}

// ChainVersion is the zos version document published on chain.
type ChainVersion struct {
SafeToUpgrade bool `json:"safe_to_upgrade"`
Version string `json:"version"`
VersionLight string `json:"version_light"`
}

// updateZosVersion updates the latest zos flist for a specific network with the updated zos version
func (w *Worker) updateZosVersion(network Network, manager client.Manager) error {
if err := checkNetwork(network); err != nil {
Expand All @@ -109,24 +116,22 @@ func (w *Worker) updateZosVersion(network Network, manager client.Manager) error
return err
}

type ChainVersion struct {
SafeToUpgrade bool `json:"safe_to_upgrade"`
Version string `json:"version"`
VersionLight string `json:"version_light"`
}

var chainVersion ChainVersion
err = json.Unmarshal([]byte(currentZosVersion), &chainVersion)
if err != nil {
if err := json.Unmarshal([]byte(currentZosVersion), &chainVersion); err != nil {
log.Debug().Err(err).Msg("failed to unmarshal chain version")
// shouldn't fail for env that still not updated version format
return nil
}

// During a canary rollout (safe_to_upgrade == false) the version is delivered only to
// the configured test farms by the node upgrader. Keep the network `latest` symlink
// pointing at the last GA version so freshly bootstrapped nodes (and non-canary nodes)
// don't pick up the canary version.
return w.applyVersion(network, chainVersion)
}

// applyVersion points the network `latest` flist links at chainVersion, but only when the
// chain marks it safe_to_upgrade. During a canary rollout (safe_to_upgrade == false) the
// link is held at the last GA version so freshly bootstrapped nodes and non-canary nodes
// don't pick up the canary version; canary farms receive it via the node upgrader, which
// targets the chain version tag directly.
func (w *Worker) applyVersion(network Network, chainVersion ChainVersion) error {
if !chainVersion.SafeToUpgrade {
log.Debug().Msgf("skipping %v latest link update: version %v is not marked safe to upgrade yet", network, chainVersion.Version)
return nil
Expand Down
105 changes: 57 additions & 48 deletions tools/zos-update-worker/internal/worker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,78 +2,87 @@ package internal

import (
"os"
"path/filepath"
"testing"
"time"

client "github.com/threefoldtech/substrate-client"
)

func TestWorker(t *testing.T) {
func newTestWorker(t *testing.T) *Worker {
t.Helper()
testDir := t.TempDir()
src := filepath.Join(testDir, "tf-autobuilder")
dst := filepath.Join(testDir, "tf-zos")

params := Params{
Interval: 1 * time.Second,
QAUrls: []string{"wss://tfchain.qa.grid.tf/ws"},
TestUrls: []string{"wss://tfchain.test.grid.tf/ws"},
MainUrls: []string{"wss://tfchain.grid.tf/ws"},
if err := os.Mkdir(src, os.ModePerm); err != nil {
t.Fatal(err)
}
src := testDir + "/tf-autobuilder"
dst := testDir + "/tf-zos"

err := os.Mkdir(src, os.ModePerm)
if err != nil {
t.Error(err)
}

err = os.Mkdir(dst, os.ModePerm)
if err != nil {
t.Error(err)
if err := os.Mkdir(dst, os.ModePerm); err != nil {
t.Fatal(err)
}

worker, err := NewWorker(src, dst, params)
worker, err := NewWorker(src, dst, Params{Interval: time.Second})
if err != nil {
t.Error(err)
t.Fatal(err)
}
return worker
}

t.Run("test_no_src_qa", func(t *testing.T) {
err := worker.updateZosVersion("qa", worker.substrate["qa"])
if err == nil {
t.Errorf("update zos should fail")
func TestApplyVersion(t *testing.T) {
// During a canary (safe_to_upgrade == false) the latest link must NOT be advanced,
// even if the source flist exists, so new/non-canary nodes stay on GA.
t.Run("gated when not safe to upgrade", func(t *testing.T) {
worker := newTestWorker(t)
const version = "v3.1.1"
if err := os.Mkdir(filepath.Join(worker.src, ".tag-"+version), os.ModePerm); err != nil {
t.Fatal(err)
}
})

t.Run("test_no_src_test", func(t *testing.T) {
_, err := os.Create(src + "/zos:v3.4.0-qa1.flist")
if err != nil {
t.Error(err)
if err := worker.applyVersion(MainNetwork, ChainVersion{Version: version, SafeToUpgrade: false}); err != nil {
t.Fatalf("expected no error when gated, got %v", err)
}

err = worker.updateZosVersion("testing", worker.substrate["testing"])
if err == nil {
t.Errorf("update zos should fail for test, %v", err)
if _, err := os.Lstat(filepath.Join(worker.dst, string(MainNetwork))); !os.IsNotExist(err) {
t.Fatalf("expected no latest link to be created while gated")
}
})

t.Run("test_no_src_main", func(t *testing.T) {
_, err = os.Create(src + "/zos:v3.1.1-rc2.flist")
if err != nil {
t.Error(err)
}

err = worker.updateZosVersion("production", worker.substrate["production"])
if err == nil {
t.Errorf("update zos should fail for main, %v", err)
// safe_to_upgrade but the source flist is missing: the link update must fail.
t.Run("fails when safe but source missing", func(t *testing.T) {
worker := newTestWorker(t)
if err := worker.applyVersion(MainNetwork, ChainVersion{Version: "v3.1.1", SafeToUpgrade: true}); err == nil {
t.Fatalf("expected error when the source flist is missing")
}
})

t.Run("test_params_wrong_url", func(t *testing.T) {
params.QAUrls = []string{"wss://tfchain.qa1.grid.tf/ws"}
// safe_to_upgrade and the source exists: the latest link is created pointing at it.
t.Run("links latest when safe and source exists", func(t *testing.T) {
worker := newTestWorker(t)
const version = "v3.1.1"
if err := os.Mkdir(filepath.Join(worker.src, ".tag-"+version), os.ModePerm); err != nil {
t.Fatal(err)
}

worker, err = NewWorker(src, dst, params)
if err := worker.applyVersion(MainNetwork, ChainVersion{Version: version, SafeToUpgrade: true}); err != nil {
t.Fatalf("expected success, got %v", err)
}
target, err := os.Readlink(filepath.Join(worker.dst, string(MainNetwork)))
if err != nil {
t.Error(err)
t.Fatalf("expected latest link to be created: %v", err)
}
err := worker.updateZosVersion("qa", worker.substrate["qa"])
if err == nil {
t.Errorf("update zos should fail")
if filepath.Base(target) != ".tag-"+version {
t.Fatalf("expected link to point at .tag-%s, got %s", version, target)
}
})
}

// TestUpdateZosVersionUnreachable checks the connect/fetch path reports an error when the
// substrate endpoint is unreachable.
func TestUpdateZosVersionUnreachable(t *testing.T) {
worker := newTestWorker(t)
worker.substrate[QANetwork] = client.NewManager("wss://tfchain.qa1.grid.tf/ws")

if err := worker.updateZosVersion(QANetwork, worker.substrate[QANetwork]); err == nil {
t.Fatalf("expected updateZosVersion to fail against an unreachable substrate url")
}
}
Loading