diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b02b2044..2fea3ab9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -14,7 +14,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: 1.24 + go-version-file: go.mod - name: Install pallas run: | diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index a28bd786..c7c25d50 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: 1.24 + go-version-file: go.mod - name: Install Build Dependencies run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9f3a00e9..66d2efc1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: 1.24 + go-version-file: go.mod - name: Install Build Dependencies run: | diff --git a/cmd/upgrade.go b/cmd/upgrade.go index b53eb8b5..d5b84f17 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -19,6 +19,7 @@ import ( "os" "strings" + digest "github.com/opencontainers/go-digest" "github.com/spf13/cobra" "github.com/vanilla-os/abroot/core" @@ -203,7 +204,7 @@ func upgrade(cmd *cobra.Command, args []string) error { } if raw { - newDigestIfHasUpdate := "" + var newDigestIfHasUpdate digest.Digest = "" if res { newDigestIfHasUpdate = newDigest } diff --git a/core/image.go b/core/image.go index 1e500851..01538694 100644 --- a/core/image.go +++ b/core/image.go @@ -19,6 +19,8 @@ import ( "os" "path/filepath" "time" + + digest "github.com/opencontainers/go-digest" ) // The ABImage is the representation of an OCI image used by ABRoot, it @@ -26,14 +28,14 @@ import ( // investigate the current ABImage on an ABRoot system, you can find it // at /abimage.abr type ABImage struct { - Digest string `json:"digest"` - Timestamp time.Time `json:"timestamp"` - Image string `json:"image"` + Digest digest.Digest `json:"digest"` + Timestamp time.Time `json:"timestamp"` + Image string `json:"image"` } // NewABImage creates a new ABImage instance and returns a pointer to it, // if the digest is empty, it returns an error -func NewABImage(digest string, image string) (*ABImage, error) { +func NewABImage(digest digest.Digest, image string) (*ABImage, error) { if digest == "" { return nil, fmt.Errorf("NewABImage: digest is empty") } diff --git a/core/oci.go b/core/oci.go index 87f7e198..42ea69be 100644 --- a/core/oci.go +++ b/core/oci.go @@ -27,6 +27,7 @@ import ( "github.com/containers/image/v5/types" "github.com/containers/storage" humanize "github.com/dustin/go-humanize" + digest "github.com/opencontainers/go-digest" "github.com/pterm/pterm" "github.com/vanilla-os/abroot/settings" "github.com/vanilla-os/prometheus" @@ -218,11 +219,13 @@ func pullImageWithProgressbar(pt *prometheus.Prometheus, name string, image *Ima progressCh := make(chan types.ProgressProperties) manifestCh := make(chan prometheus.OciManifest) + errorCh := make(chan error) defer close(progressCh) defer close(manifestCh) + defer close(errorCh) - err := pt.PullImageAsync(image.From, name, progressCh, manifestCh) + err := pt.PullImageAsync(image.From, name, progressCh, manifestCh, errorCh) if err != nil { PrintVerboseErr("pullImageWithProgressbar", 0, err) return err @@ -360,3 +363,36 @@ func DeleteAllButLatestImage() error { return nil } + +// HasUpdate checks if the image/tag from the registry has a different digest +// it returns the new digest and a boolean indicating if an update is available +func HasUpdate(oldDigest digest.Digest) (digest.Digest, bool, error) { + PrintVerboseInfo("OCI.HasUpdate", "Checking for updates ...") + + pt, err := prometheus.NewPrometheus( + "/var/lib/abroot/storage", + "overlay", + settings.Cnf.MaxParallelDownloads, + ) + if err != nil { + PrintVerboseErr("OCI.HasUpdate", 0, err) + return "", false, err + } + + imageName := fmt.Sprintf("%s/%s:%s", settings.Cnf.Registry, settings.Cnf.Name, settings.Cnf.Tag) + PrintVerboseInfo("OCI.HasUpdate", "checking image: ", imageName) + + _, newDigest, err := pt.PullManifestOnly(imageName) + if err != nil { + PrintVerboseErr("OCI.HasUpdate", 1, err) + return "", false, err + } + + if newDigest == oldDigest { + PrintVerboseInfo("OCI.HasUpdate", "no update available") + return "", false, nil + } + + PrintVerboseInfo("OCI.HasUpdate", "update available. Old digest: ", oldDigest, ", new digest: ", newDigest) + return newDigest, true, nil +} diff --git a/core/package-diff.go b/core/package-diff.go index 8868bdb7..a2f364cd 100644 --- a/core/package-diff.go +++ b/core/package-diff.go @@ -20,6 +20,7 @@ import ( "net/http" "strings" + digest "github.com/opencontainers/go-digest" "github.com/vanilla-os/abroot/extras/dpkg" "github.com/vanilla-os/abroot/settings" "github.com/vanilla-os/differ/diff" @@ -27,7 +28,7 @@ import ( // BaseImagePackageDiff retrieves the added, removed, upgraded and downgraded // base packages (the ones bundled with the image). -func BaseImagePackageDiff(currentDigest, newDigest string) ( +func BaseImagePackageDiff(currentDigest, newDigest digest.Digest) ( added, upgraded, downgraded, removed []diff.PackageDiff, err error, ) { diff --git a/core/registry.go b/core/registry.go deleted file mode 100644 index 05e9cd51..00000000 --- a/core/registry.go +++ /dev/null @@ -1,229 +0,0 @@ -package core - -/* License: GPLv3 - Authors: - Mirko Brombin - Vanilla OS Contributors - Copyright: 2024 - Description: - ABRoot is utility which provides full immutability and - atomicity to a Linux system, by transacting between - two root filesystems. Updates are performed using OCI - images, to ensure that the system is always in a - consistent state. -*/ -import ( - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "strings" - - "github.com/vanilla-os/abroot/settings" -) - -// A Registry instance exposes functions to interact with the configured -// Docker registry -type Registry struct { - API string -} - -// Manifest is the struct used to parse the manifest response from the registry -// it contains the manifest itself, the digest and the list of layers. This -// should be compatible with most registries, but it's not guaranteed -type Manifest struct { - Manifest []byte - Digest string - Layers []string -} - -var ErrImageNotFound error = errors.New("configured image cannot be found") - -// NewRegistry returns a new Registry instance, exposing functions to -// interact with the configured Docker registry -func NewRegistry() *Registry { - PrintVerboseInfo("NewRegistry", "running...") - return &Registry{ - API: fmt.Sprintf("https://%s/%s", settings.Cnf.Registry, settings.Cnf.RegistryAPIVersion), - } -} - -// HasUpdate checks if the image/tag from the registry has a different digest -// it returns the new digest and a boolean indicating if an update is available -func (r *Registry) HasUpdate(digest string) (string, bool, error) { - PrintVerboseInfo("Registry.HasUpdate", "Checking for updates ...") - - token, err := GetToken() - if err != nil { - PrintVerboseErr("Registry.HasUpdate", 0, err) - return "", false, err - } - - manifest, err := r.GetManifest(token) - if err != nil { - PrintVerboseErr("Registry.HasUpdate", 1, err) - return "", false, err - } - - if manifest.Digest == digest { - PrintVerboseInfo("Registry.HasUpdate", "no update available") - return "", false, nil - } - - PrintVerboseInfo("Registry.HasUpdate", "update available. Old digest", digest, "new digest", manifest.Digest) - return manifest.Digest, true, nil -} - -func getRegistryAuthUrl() (string, string, error) { - requestUrl := fmt.Sprintf( - "https://%s/%s/", - settings.Cnf.Registry, - settings.Cnf.RegistryAPIVersion, - ) - - resp, err := http.Get(requestUrl) - if err != nil { - return "", "", err - } - if resp.StatusCode == 401 { - authUrl := resp.Header["www-authenticate"] - if len(authUrl) == 0 { - authUrl = resp.Header["Www-Authenticate"] - if len(authUrl) == 0 { - return "", "", fmt.Errorf("unable to find authentication url for registry") - } - } - return strings.Split(strings.Split(authUrl[0], "realm=\"")[1], "\",")[0], strings.Split(strings.Split(authUrl[0], "service=\"")[1], "\"")[0], nil - } else { - PrintVerboseInfo("Registry.getRegistryAuthUrl", "registry does not require authentication") - return fmt.Sprintf("https://%s/", settings.Cnf.Registry), settings.Cnf.RegistryService, nil - } -} - -// GetToken generates a token using the provided tokenURL and returns it -func GetToken() (string, error) { - authUrl, serviceUrl, err := getRegistryAuthUrl() - if err != nil { - return "", err - } - requestUrl := fmt.Sprintf( - "%s?scope=repository:%s:pull&service=%s", - authUrl, - settings.Cnf.Name, - serviceUrl, - ) - PrintVerboseInfo("Registry.GetToken", "call URI is", requestUrl) - - resp, err := http.Get(requestUrl) - if err != nil { - return "", err - } - defer resp.Body.Close() - - if resp.StatusCode == http.StatusForbidden { - return "", ErrImageNotFound - } - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("token request failed with status code: %d", resp.StatusCode) - } - - tokenBytes, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - // Parse token from response - var tokenResponse struct { - Token string `json:"token"` - } - err = json.Unmarshal(tokenBytes, &tokenResponse) - if err != nil { - return "", err - } - - token := tokenResponse.Token - return token, nil -} - -// GetManifest returns the manifest of the image, a token is required -// to perform the request and is generated using GetToken() -func (r *Registry) GetManifest(token string) (*Manifest, error) { - PrintVerboseInfo("Registry.GetManifest", "running...") - - manifestAPIUrl := fmt.Sprintf("%s/%s/manifests/%s", r.API, settings.Cnf.Name, settings.Cnf.Tag) - PrintVerboseInfo("Registry.GetManifest", "call URI is", manifestAPIUrl) - - req, err := http.NewRequest("GET", manifestAPIUrl, nil) - if err != nil { - PrintVerboseErr("Registry.GetManifest", 0, err) - return nil, err - } - - req.Header.Set("User-Agent", "abroot") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - req.Header.Set("Accept", "application/vnd.oci.image.manifest.v1+json") - - resp, err := http.DefaultClient.Do(req) - if err != nil { - PrintVerboseErr("Registry.GetManifest", 1, err) - return nil, err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - PrintVerboseErr("Registry.GetManifest", 2, err) - return nil, err - } - - m := make(map[string]interface{}) - err = json.Unmarshal(body, &m) - if err != nil { - PrintVerboseErr("Registry.GetManifest", 3, err) - return nil, err - } - - // If the manifest contains an errors property, it means that the - // request failed. Ref: https://github.com/Vanilla-OS/ABRoot/issues/285 - if m["errors"] != nil { - errors := m["errors"].([]interface{}) - for _, e := range errors { - err := e.(map[string]interface{}) - PrintVerboseErr("Registry.GetManifest", 3.5, err) - return nil, fmt.Errorf("Registry error: %s", err["code"]) - } - } - - // digest is stored in the header - digest := resp.Header.Get("Docker-Content-Digest") - - // we need to parse the layers to get the digests - var layerDigests []string - if m["layers"] == nil && m["fsLayers"] == nil { - PrintVerboseErr("Registry.GetManifest", 4, err) - return nil, fmt.Errorf("Manifest does not contain layer property") - } else if m["layers"] == nil && m["fsLayers"] != nil { - PrintVerboseWarn("Registry.GetManifest", 4, "layers property not found, using fsLayers") - layers := m["fsLayers"].([]interface{}) - for _, layer := range layers { - layerDigests = append(layerDigests, layer.(map[string]interface{})["blobSum"].(string)) - } - } else { - layers := m["layers"].([]interface{}) - var layerDigests []string - for _, layer := range layers { - layerDigests = append(layerDigests, layer.(map[string]interface{})["digest"].(string)) - } - } - - PrintVerboseInfo("Registry.GetManifest", "success") - manifest := &Manifest{ - Manifest: body, - Digest: digest, - Layers: layerDigests, - } - - return manifest, nil -} diff --git a/core/system.go b/core/system.go index eb1cfc5c..f12fcdfd 100644 --- a/core/system.go +++ b/core/system.go @@ -24,6 +24,7 @@ import ( "github.com/google/uuid" EtcBuilder "github.com/linux-immutability-tools/EtcBuilder/cmd" + digest "github.com/opencontainers/go-digest" "github.com/vanilla-os/abroot/settings" "github.com/vanilla-os/sdk/pkg/v1/goodies" ) @@ -40,10 +41,6 @@ type ABSystem struct { // manage the ABRoot partition scheme. RootM *ABRootManager - // Registry contains an instance of the Registry used to retrieve resources - // from the configured Docker registry. - Registry *Registry - // CurImage contains an instance of ABImage which represents the current // image used by the system (abimage.abr). CurImage *ABImage @@ -107,13 +104,11 @@ func NewABSystem() (*ABSystem, error) { } c := NewChecks() - r := NewRegistry() rm := NewABRootManager() return &ABSystem{ Checks: c, RootM: rm, - Registry: r, CurImage: i, }, nil } @@ -133,9 +128,9 @@ func (s *ABSystem) CheckAll() error { } // CheckUpdate checks if there is an update available -func (s *ABSystem) CheckUpdate() (string, bool, error) { +func (s *ABSystem) CheckUpdate() (digest.Digest, bool, error) { PrintVerboseInfo("ABSystem.CheckUpdate", "running...") - return s.Registry.HasUpdate(s.CurImage.Digest) + return HasUpdate(s.CurImage.Digest) } func (s *ABSystem) CreateRootSymlinks(systemNewPath string) error { @@ -185,8 +180,8 @@ func (s *ABSystem) Rebase(name string, dryRun bool) error { } _, _, err := s.CheckUpdate() - if errors.Is(err, ErrImageNotFound) { - return fmt.Errorf("provided image cannot be found") + if err != nil { + return err } if !dryRun { @@ -252,7 +247,7 @@ func (s *ABSystem) RunOperation(operation ABSystemOperation, freeSpace bool) err return err } - var imageDigest string + var imageDigest digest.Digest if operation != INITRAMFS { var res bool imageDigest, res, err = s.CheckUpdate() @@ -390,8 +385,8 @@ func (s *ABSystem) RunOperation(operation ABSystemOperation, freeSpace bool) err } default: imageName = settings.GetFullImageName() - imageName += "@" + imageDigest - labels["ABRoot.BaseImageDigest"] = imageDigest + imageName += "@" + imageDigest.String() + labels["ABRoot.BaseImageDigest"] = imageDigest.String() } imageRecipe := NewImageRecipe( diff --git a/go.mod b/go.mod index 0555fa22..d8fee12b 100644 --- a/go.mod +++ b/go.mod @@ -1,22 +1,23 @@ module github.com/vanilla-os/abroot -go 1.24 +go 1.24.4 require ( - github.com/containers/buildah v1.41.4 + github.com/containers/buildah v1.41.5 github.com/containers/image/v5 v5.36.2 github.com/containers/storage v1.59.1 github.com/dustin/go-humanize v1.0.1 github.com/google/uuid v1.6.0 github.com/hashicorp/go-version v1.7.0 github.com/linux-immutability-tools/EtcBuilder v1.3.0 + github.com/opencontainers/go-digest v1.0.0 github.com/pterm/pterm v0.12.79 github.com/shirou/gopsutil v3.21.11+incompatible github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.19.0 github.com/vanilla-os/differ/diff v0.0.0-20240202135932-673de99cc540 github.com/vanilla-os/orchid v0.6.1 - github.com/vanilla-os/prometheus v1.0.2 + github.com/vanilla-os/prometheus v1.2.0 github.com/vanilla-os/sdk v0.0.0-20240424182549-7fbf2ce02046 golang.org/x/sys v0.34.0 ) @@ -108,7 +109,6 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/cgroups v0.0.4 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opencontainers/runc v1.3.0 // indirect github.com/opencontainers/runtime-spec v1.2.1 // indirect diff --git a/go.sum b/go.sum index a8f281db..ac61fe00 100644 --- a/go.sum +++ b/go.sum @@ -77,8 +77,8 @@ github.com/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEm github.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4= github.com/containernetworking/plugins v1.7.1 h1:CNAR0jviDj6FS5Vg85NTgKWLDzZPfi/lj+VJfhMDTIs= github.com/containernetworking/plugins v1.7.1/go.mod h1:xuMdjuio+a1oVQsHKjr/mgzuZ24leAsqUYRnzGoXHy0= -github.com/containers/buildah v1.41.4 h1:IHYWex7rwhsOwtRXQ+VMEQr96gUbSbSvxJcX6AoiDeA= -github.com/containers/buildah v1.41.4/go.mod h1:IFW8MbAgXYiUBCcAFExlHkPfE41DJWVBCbDZWZ9WEng= +github.com/containers/buildah v1.41.5 h1:tdxtsb+SctAQ0/vdAJg5AMArVypeN2DmIjHV1bkoMO4= +github.com/containers/buildah v1.41.5/go.mod h1:IFW8MbAgXYiUBCcAFExlHkPfE41DJWVBCbDZWZ9WEng= github.com/containers/common v0.64.2 h1:1xepE7QwQggUXxmyQ1Dbh6Cn0yd7ktk14sN3McSWf5I= github.com/containers/common v0.64.2/go.mod h1:o29GfYy4tefUuShm8mOn2AiL5Mpzdio+viHI7n24KJ4= github.com/containers/image/v5 v5.36.2 h1:GcxYQyAHRF/pLqR4p4RpvKllnNL8mOBn0eZnqJbfTwk= @@ -420,8 +420,8 @@ github.com/vanilla-os/differ/diff v0.0.0-20240202135932-673de99cc540 h1:KrNjRudM github.com/vanilla-os/differ/diff v0.0.0-20240202135932-673de99cc540/go.mod h1:HMg24arXCutcwngVaJ4DQuhwLmS8CA/CuVSjEyIxFpw= github.com/vanilla-os/orchid v0.6.1 h1:lY1QIhpyzB83N/KdrYuw3fgT+BygyhsO7RKnInpY6gU= github.com/vanilla-os/orchid v0.6.1/go.mod h1:dNPvHxofO4hEXodEKXp0nLQDZhoHh8evCUXc6X1xLao= -github.com/vanilla-os/prometheus v1.0.2 h1:KPAr3D3tjDCoztoP/xZsreBu6aGNajHo2/XhjDGvThg= -github.com/vanilla-os/prometheus v1.0.2/go.mod h1:O18GdRYve4MfvDjV4qwezTvXWFgwAt7AmS8wbfZqTUg= +github.com/vanilla-os/prometheus v1.2.0 h1:Tt5kXL98cM6PLNt/oQcQHyWDoUNVIITuTOFqrJzITzU= +github.com/vanilla-os/prometheus v1.2.0/go.mod h1:S589rjrDSx8KsUzyU7qxPFIRA9/zV+kQPYSn+DnW/uw= github.com/vanilla-os/sdk v0.0.0-20240424182549-7fbf2ce02046 h1:FYVQ7Suwq2O7D7Nm1XiWcy78M5z2PHd/HZJYFBiJ9HM= github.com/vanilla-os/sdk v0.0.0-20240424182549-7fbf2ce02046/go.mod h1:AgO1AGmVqEhFK3ptSD4ZeKuL0xyTh3iBeRv0edhOSh4= github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= diff --git a/tests/pkg_test.go b/tests/pkg_test.go index baa14a72..7ac3c98a 100644 --- a/tests/pkg_test.go +++ b/tests/pkg_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + digest "github.com/opencontainers/go-digest" "github.com/vanilla-os/abroot/core" "github.com/vanilla-os/abroot/settings" ) @@ -65,8 +66,8 @@ func TestPackageManager(t *testing.T) { func TestBaseImagePackageDiff(t *testing.T) { settings.Cnf.Name = "vanilla-os/core" - oldDigest := "sha256:eac5693376d75cee2e676a83a67f4ce5db17d21e30bbde6a752480928719c842" - newDigest := "sha256:eaa30f5a907f6f7785936a31f94fe291c6ce00943dcd1d3a8a6e40f1fc890346" + oldDigest := digest.FromString("sha256:eac5693376d75cee2e676a83a67f4ce5db17d21e30bbde6a752480928719c842") + newDigest := digest.FromString("sha256:eaa30f5a907f6f7785936a31f94fe291c6ce00943dcd1d3a8a6e40f1fc890346") added, upgraded, downgraded, removed, err := core.BaseImagePackageDiff(oldDigest, newDigest) if err != nil { diff --git a/tests/registry_test.go b/tests/registry_test.go deleted file mode 100644 index c840878b..00000000 --- a/tests/registry_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package tests - -import ( - "testing" - - "github.com/vanilla-os/abroot/core" -) - -// TestRegistryHasUpdate tests the HasUpdate function by checking if the -// registry has an update with a digest different than our current one. -func TestRegistryHasUpdate(t *testing.T) { - t.Log("TestRegistryHasUpdate: running...") - - registry := core.NewRegistry() - if registry == nil { - t.Fatal("TestRegistryHasUpdate: registry is nil") - } - - digest, update, err := registry.HasUpdate("impossible_digest") - if err != nil { - t.Fatal(err) - } - if digest == "" && update == false { - t.Fatal("TestRegistryHasUpdate: digest and update are empty") - } - - t.Logf("TestRegistryHasUpdate: digest: %s, update: %t", digest, update) - t.Log("TestRegistryHasUpdate: done") -} - -// TestRegistryGetToken tests the GetToken function by getting a token from the -// registry to authenticate the requests. -func TestRegistryGetToken(t *testing.T) { - t.Log("TestRegistryGetToken: running...") - - token, err := core.GetToken() - if err != nil { - t.Fatal(err) - } - - if token == "" { - t.Fatal("TestRegistryGetToken: token is empty") - } - - t.Log("TestRegistryGetToken: done") -} - -func TestRegistryGetManifest(t *testing.T) { - t.Log("TestRegistryGetManifest: running...") - - token, err := core.GetToken() - if err != nil { - t.Fatal(err) - } - - registry := core.NewRegistry() - if registry == nil { - t.Fatal("TestRegistryGetManifest: registry is nil") - } - - manifest, err := registry.GetManifest(token) - if err != nil { - t.Fatal(err) - } - - if manifest.Digest == "" { - t.Fatal("TestRegistryGetManifest: manifest.Digest is empty") - } - - t.Logf("TestRegistryGetManifest: manifest.Digest: %s", manifest.Digest) - t.Log("TestRegistryGetManifest: done") -}