Initial commit

ubuntu/plucky
Simon Quigley 1 week ago
commit e57ad37808

5
debian/changelog vendored

@ -0,0 +1,5 @@
snapd-extra-utils (1.0.0) UNRELEASED; urgency=medium
* Initial release. (Closes: #nnnn) <nnnn is the bug number of your ITP>
-- Simon Quigley <tsimonq2@ubuntu.com> Mon, 14 Oct 2024 13:51:43 -0500

30
debian/control vendored

@ -0,0 +1,30 @@
Source: snapd-extra-utils
Section: devel
Priority: optional
Maintainer: Simon Quigley <tsimonq2@ubuntu.com>
Rules-Requires-Root: no
Build-Depends: debhelper-compat (= 13),
golang-github-snapcore-snapd-dev (>= 2.62),
golang-go,
golang-golang-x-crypto-dev,
golang-gopkg-yaml.v3-dev,
qt6-base-dev
Standards-Version: 4.7.0
#Vcs-Browser: https://salsa.debian.org/debian/snapd-extra-utils
#Vcs-Git: https://salsa.debian.org/debian/snapd-extra-utils.git
Package: snapd-seed-glue
Architecture: any
Depends: snapd, ${misc:Depends}, ${shlibs:Depends}
Description: Installer and pre-seed utilities for snapd
Primarily used in Calamares, snapd-seed-glue updates snap seeds in a given
directory, on a pre-booted system. It handles dependency resolution, delta
downloads, and full snap downloads.
Package: snapd-installation-monitor
Architecture: any
Depends: snapd, ${misc:Depends}, ${shlibs:Depends}
Description: First-boot snap install notification
When many snaps are preseeded, on first boot the user may be confused if they
can not open one of those snaps. This simple notification informs the user,
disappears when it is complete, and usually is not shown again.

24
debian/copyright vendored

@ -0,0 +1,24 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: snapd-extra-utils
Upstream-Contact: Simon Quigley <tsimonq2@ubuntu.com>
Files: *
Copyright: 2024 Simon Quigley <tsimonq2@ubuntu.com>
License: GPL-3.0+
License: GPL-3.0+
This package is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
.
This package is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
.
You should have received a copy of the GNU General Public License
along with this package. If not, see <https://www.gnu.org/licenses/>.
Comment:
On Debian systems, the complete text of the GNU General
Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".

12
debian/rules vendored

@ -0,0 +1,12 @@
#!/usr/bin/make -f
export DH_VERBOSE = 1
export GO111MODULE = off
export GOPATH=/usr/share/gocode
export GOCACHE=$(CURDIR)/.gocache
%:
dh $@
override_dh_auto_build:
(cd snapd-seed-glue && go build -gcflags="all=-N -l" -ldflags="-compressdwarf=false" -o snapd-seed-glue)
(cd snapd-installation-monitor && cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo . && make)

@ -0,0 +1,2 @@
snapd-installation-monitor/snapd-installation-monitor /usr/libexec
snapd-installation-monitor/snapd-installation-monitor.desktop /etc/xdg/autostart

@ -0,0 +1 @@
snapd-seed-glue/snapd-seed-glue /usr/bin/

@ -0,0 +1 @@
3.0 (native)

@ -0,0 +1,65 @@
cmake_minimum_required(VERSION 3.16...3.21)
# These are part of the public API. Projects should use them to provide a
# consistent set of prefix-relative destinations.
if(NOT QT_DEPLOY_BIN_DIR)
set(QT_DEPLOY_BIN_DIR "bin")
endif()
if(NOT QT_DEPLOY_LIB_DIR)
set(QT_DEPLOY_LIB_DIR "lib")
endif()
if(NOT QT_DEPLOY_PLUGINS_DIR)
set(QT_DEPLOY_PLUGINS_DIR "plugins")
endif()
if(NOT QT_DEPLOY_QML_DIR)
set(QT_DEPLOY_QML_DIR "qml")
endif()
if(NOT QT_DEPLOY_TRANSLATIONS_DIR)
set(QT_DEPLOY_TRANSLATIONS_DIR "translations")
endif()
if(NOT QT_DEPLOY_PREFIX)
set(QT_DEPLOY_PREFIX "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}")
endif()
if(QT_DEPLOY_PREFIX STREQUAL "")
set(QT_DEPLOY_PREFIX .)
endif()
if(NOT QT_DEPLOY_IGNORED_LIB_DIRS)
set(QT_DEPLOY_IGNORED_LIB_DIRS "/lib/x86_64-linux-gnu;/lib")
endif()
# These are internal implementation details. They may be removed at any time.
set(__QT_DEPLOY_SYSTEM_NAME "Linux")
set(__QT_DEPLOY_IS_SHARED_LIBS_BUILD "ON")
set(__QT_DEPLOY_TOOL "GRD")
set(__QT_DEPLOY_IMPL_DIR "/home/tsimonq2/Code/25.04/snapd-extra-utils/snap-installation-monitor/.qt")
set(__QT_DEPLOY_VERBOSE "")
set(__QT_CMAKE_EXPORT_NAMESPACE "Qt6")
set(__QT_DEPLOY_GENERATOR_IS_MULTI_CONFIG "0")
set(__QT_DEPLOY_ACTIVE_CONFIG "Release")
set(__QT_NO_CREATE_VERSIONLESS_FUNCTIONS "")
set(__QT_DEFAULT_MAJOR_VERSION "6")
set(__QT_DEPLOY_QT_ADDITIONAL_PACKAGES_PREFIX_PATH "")
set(__QT_DEPLOY_QT_INSTALL_PREFIX "/usr")
set(__QT_DEPLOY_QT_INSTALL_BINS "lib/qt6/bin")
set(__QT_DEPLOY_QT_INSTALL_DATA "share/qt6")
set(__QT_DEPLOY_QT_INSTALL_LIBEXECS "lib/qt6/libexec")
set(__QT_DEPLOY_QT_INSTALL_PLUGINS "lib/x86_64-linux-gnu/qt6/plugins")
set(__QT_DEPLOY_QT_INSTALL_TRANSLATIONS "share/qt6/translations")
set(__QT_DEPLOY_TARGET_QT_PATHS_PATH "/usr/lib/qt6/bin/qtpaths")
set(__QT_DEPLOY_PLUGINS "")
set(__QT_DEPLOY_MUST_ADJUST_PLUGINS_RPATH "ON")
set(__QT_DEPLOY_USE_PATCHELF "")
set(__QT_DEPLOY_PATCHELF_EXECUTABLE "")
set(__QT_DEPLOY_QT_IS_MULTI_CONFIG_BUILD_WITH_DEBUG "FALSE")
set(__QT_DEPLOY_QT_DEBUG_POSTFIX "")
# Define the CMake commands to be made available during deployment.
set(__qt_deploy_support_files
"/usr/lib/x86_64-linux-gnu/cmake/Qt6Core/Qt6CoreDeploySupport.cmake"
)
foreach(__qt_deploy_support_file IN LISTS __qt_deploy_support_files)
include("${__qt_deploy_support_file}")
endforeach()
unset(__qt_deploy_support_file)
unset(__qt_deploy_support_files)

@ -0,0 +1,12 @@
cmake_minimum_required(VERSION 3.5.0)
project(snapd-installation-monitor)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 COMPONENTS Widgets DBus REQUIRED)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(snapd-installation-monitor main.cpp)
target_link_libraries(snapd-installation-monitor Qt6::Widgets Qt6::DBus)

@ -0,0 +1,76 @@
// Copyright (C) 2024 Simon Quigley <tsimonq2@ubuntu.com>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 3
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
#include <QApplication>
#include <QSystemTrayIcon>
#include <QtDBus/QtDBus>
#include <QTimer>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
// Create and set application icon
QIcon appIcon = QIcon::fromTheme("dialog-information");
QApplication::setWindowIcon(appIcon);
// DBus interface to systemd
QDBusInterface systemd("org.freedesktop.systemd1",
"/org/freedesktop/systemd1",
"org.freedesktop.systemd1.Manager",
QDBusConnection::systemBus());
// Retrieve current status of snapd.seeded.service, which tracks the preseed process
QDBusMessage methodCall = systemd.call("GetUnit", "snapd.seeded.service");
QDBusObjectPath unitPath = methodCall.arguments().at(0).value<QDBusObjectPath>();
QDBusInterface unit("org.freedesktop.systemd1",
unitPath.path(),
"org.freedesktop.systemd1.Unit",
QDBusConnection::systemBus());
QVariant activeState = unit.property("ActiveState");
QVariant subState = unit.property("SubState");
// System tray icon setup
QSystemTrayIcon trayIcon(appIcon);
trayIcon.setToolTip("Snap Installation Monitor");
// Initial message displayed in the system tray
auto showMessage = [&trayIcon]() {
trayIcon.showMessage("Installation Notice", "Finalizing installation of snaps, please wait...",
QSystemTrayIcon::Information, 15000);
};
// If the user clicks the system tray icon, display the notification again
QObject::connect(&trayIcon, &QSystemTrayIcon::activated, [&](QSystemTrayIcon::ActivationReason reason) {
if (reason == QSystemTrayIcon::Trigger) {
showMessage();
}
});
// Exit immediately if the service is "active (exited)", launch the GUI parts otherwise
if (activeState.toString() == "active" && subState.toString() == "exited") { return 0; }
trayIcon.show();
showMessage();
QTimer timer;
QObject::connect(&timer, &QTimer::timeout, [&unit, &trayIcon]() {
QVariant newState = unit.property("ActiveState");
QVariant newSubState = unit.property("SubState");
if (newState.toString() == "active" && newSubState.toString() == "exited") {
trayIcon.hide();
QApplication::quit();
}
});
timer.start(5000); // Check every 5 seconds
return app.exec();
}

@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name=Snap Installation Monitor
Exec=/usr/libexec/snapd-installation-monitor
Icon=dialog-information
Comment=Monitor snapd seeding at startup
X-LXQt-Need-Tray=true
NoDisplay=true

@ -0,0 +1,225 @@
package main
import (
"encoding/base64"
"encoding/hex"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/store"
"gopkg.in/yaml.v3"
)
// downloadAssertions dynamically fetches the necessary assertions and saves them to a file.
// It ensures that the account-key assertion is written first in the .assert file.
func downloadAssertions(storeClient *store.Store, snapInfo *snap.Info, downloadDir string) error {
// Define the path for the assertions file
assertionsPath := filepath.Join(downloadDir, fmt.Sprintf("%s_%d.assert", snapInfo.SuggestedName, snapInfo.Revision.N))
// Extract necessary fields from snapInfo
snapSHA := snapInfo.Sha3_384
snapID := snapInfo.SnapID
publisherID := snapInfo.Publisher.ID
series := "16" // Consider making this dynamic if possible
// Define assertion types
assertionTypes := map[string]*asserts.AssertionType{
"snap-revision": asserts.SnapRevisionType,
"snap-declaration": asserts.SnapDeclarationType,
"account-key": asserts.AccountKeyType,
"account": asserts.AccountType,
}
// Open the assertions file for writing
assertionsFile, err := os.Create(assertionsPath)
if err != nil {
return fmt.Errorf("failed to create assertions file: %w", err)
}
defer assertionsFile.Close()
// Step 1: Fetch snap-declaration assertion
snapDecl, err := storeClient.Assertion(assertionTypes["snap-declaration"], []string{series, snapID}, nil)
if err != nil {
return fmt.Errorf("failed to fetch snap-declaration assertion for snap %s: %w", snapInfo.SuggestedName, err)
}
// Step 2: Extract sign-key-sha3-384 from snap-declaration
signKey, ok := snapDecl.Header("sign-key-sha3-384").(string)
if !ok || signKey == "" {
return fmt.Errorf("snap-declaration assertion missing 'sign-key-sha3-384' header for snap %s", snapInfo.SuggestedName)
}
// Step 3: Fetch account-key assertion using sign-key-sha3-384 (no decoding)
accountKeyAssertion, err := storeClient.Assertion(assertionTypes["account-key"], []string{signKey}, nil)
if err != nil {
return fmt.Errorf("failed to fetch account-key assertion for snap %s: %w", snapInfo.SuggestedName, err)
}
// Step 4: Fetch account assertion using publisher-id
accountAssertion, err := storeClient.Assertion(assertionTypes["account"], []string{publisherID}, nil)
if err != nil {
return fmt.Errorf("failed to fetch account assertion for snap %s: %w", snapInfo.SuggestedName, err)
}
// Step 5: Fetch snap-revision assertion
snapSHA384Bytes, err := hex.DecodeString(snapSHA)
if err != nil {
return fmt.Errorf("error decoding SHA3-384 hex string for snap %s: %w", snapInfo.SuggestedName, err)
}
snapSHA384Base64 := base64.RawURLEncoding.EncodeToString(snapSHA384Bytes)
//revisionKey := fmt.Sprintf("%s/global-upload", snapSHA384Base64)
revisionKey := fmt.Sprintf("%s/", snapSHA384Base64)
snapRevisionAssertion, err := storeClient.Assertion(assertionTypes["snap-revision"], []string{revisionKey}, nil)
if err != nil {
verboseLog("Failed to fetch snap-revision assertion for snap %s: %v", snapInfo.SuggestedName, err)
// Proceeding without snap-revision might be acceptable based on your use-case
}
// Step 6: Write assertions in the desired order
// 1. account-key
writeAssertion("account-key", accountKeyAssertion, assertionsFile)
// 2. account
writeAssertion("account", accountAssertion, assertionsFile)
// 3. snap-declaration
writeAssertion("snap-declaration", snapDecl, assertionsFile)
// 4. snap-revision (if fetched successfully)
if snapRevisionAssertion != nil {
writeAssertion("snap-revision", snapRevisionAssertion, assertionsFile)
}
verboseLog("Assertions downloaded and saved to: %s", assertionsPath)
return nil
}
func writeAssertion(assertionType string, assertion asserts.Assertion, file *os.File) {
fieldOrder := map[string][]string{
"account-key": {
"type", "authority-id", "revision", "public-key-sha3-384",
"account-id", "name", "since", "body-length", "sign-key-sha3-384",
},
"account": {
"type", "authority-id", "revision", "account-id", "display-name",
"timestamp", "username", "validation", "sign-key-sha3-384",
},
"snap-declaration": {
"type", "format", "authority-id", "revision", "series", "snap-id",
"aliases", "auto-aliases", "plugs", "publisher-id", "slots",
"snap-name", "timestamp", "sign-key-sha3-384",
},
"snap-revision": {
"type", "authority-id", "snap-sha3-384", "developer-id",
"provenance", "snap-id", "snap-revision", "snap-size",
"timestamp", "sign-key-sha3-384",
},
}
body := assertion.Body()
bodyLength := len(body)
headers := assertion.Headers()
// Only write the account assertion if it is not Canonical
if assertionType == "account" {
value, exists := headers["username"]
if exists && value == "canonical" {
return
}
}
// provenance seems to be a field only available in newer snap revisions
// For snaps published in 2023 or earlier, do not include this field
timestamp, exists := headers["timestamp"]
if assertionType == "snap-revision" && exists {
layout := time.RFC3339
parsedTime, _ := time.Parse(layout, timestamp.(string))
thresholdTime := time.Date(2023, time.December, 9, 0, 0, 0, 0, time.UTC)
if parsedTime.Before(thresholdTime) || parsedTime.Equal(thresholdTime) {
delete(headers, "provenance")
}
}
// Write headers in the specified order
for _, key := range fieldOrder[assertionType] {
value, exists := headers[key]
if !exists || value == "" {
continue
}
if key == "type" {
fmt.Fprintf(file, "%s: %s\n", key, assertionType)
} else if key == "body-length" && bodyLength > 0 {
file.WriteString(fmt.Sprintf("body-length: %d\n", bodyLength))
continue
} else if isComplexField(key) {
fmt.Fprintf(file, "%s:\n", key)
serializeComplexField(value, file)
} else {
fmt.Fprintf(file, "%s: %s\n", key, value)
}
}
file.WriteString("\n")
// Write the body if it exists
if bodyLength > 0 {
file.Write(body)
file.WriteString("\n\n")
}
// Write the signature
_, signature := assertion.Signature()
file.Write(signature)
if assertionType != "snap-revision" {
file.WriteString("\n")
}
}
func serializeComplexField(value interface{}, file *os.File) {
var buf strings.Builder
encoder := yaml.NewEncoder(&buf)
encoder.SetIndent(2)
defer encoder.Close()
// Encode the value directly
if err := encoder.Encode(value); err != nil {
log.Fatalf("Error encoding YAML: %v", err)
}
// Write the serialized YAML to the file with proper indentation
lines := strings.Split(buf.String(), "\n")
for _, line := range lines {
if line == "" {
continue
}
line = strings.ReplaceAll(line, `"true"`, "true")
line = strings.ReplaceAll(line, `"false"`, "false")
line = strings.ReplaceAll(line, `'*'`, "*")
// Check for dashes indicating list items
if strings.HasPrefix(strings.TrimSpace(line), "-") && strings.Contains(line, ":") {
before, after, found := strings.Cut(line, "- ")
if found {
file.WriteString(fmt.Sprintf(" %s-\n", before))
if after != "" {
file.WriteString(fmt.Sprintf(" %s%s\n", before, after))
}
} else {
file.WriteString(fmt.Sprintf(" %s-\n", line))
}
} else if strings.TrimSpace(line) != "" {
// For any other non-empty lines, indent them correctly
file.WriteString(fmt.Sprintf(" %s\n", line))
}
}
}
// isComplexField checks if a field is complex (nested) in YAML
func isComplexField(key string) bool {
return key == "aliases" || key == "auto-aliases" || key == "plugs" || key == "slots" || key == "allow-installation" || key == "allow-connection"
}

@ -0,0 +1,152 @@
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/snapcore/snapd/store"
)
// cleanUpFiles removes partial, old, and orphaned snap and assertion files from the download and assertions directories.
func cleanUpFiles(snapsDir, assertionsDir, seedYaml string) {
verboseLog("Starting cleanup process...")
// Load the seed.yaml data
seedData := loadSeedData(seedYaml)
// Create a map of valid snap and assertion files based on seed.yaml
validSnaps := make(map[string]bool)
validAssertions := make(map[string]bool)
// Populate valid snaps and assertions from the seed.yaml data
for _, snap := range seedData.Snaps {
// Ensure correct extraction of revision
revision := extractRevisionFromFile(snap.File)
if revision == "" {
verboseLog("Failed to extract revision from file name: %s", snap.File)
continue
}
snapFileName := fmt.Sprintf("%s_%s.snap", snap.Name, revision)
assertionFileName := fmt.Sprintf("%s_%s.assert", snap.Name, revision)
validSnaps[snapFileName] = true
validAssertions[assertionFileName] = true
}
// Log valid snaps and assertions
verboseLog("Valid Snaps: %v", validSnaps)
verboseLog("Valid Assertions: %v", validAssertions)
// Remove outdated or partial snap files
files, err := os.ReadDir(snapsDir)
if err != nil {
verboseLog("Error reading snaps directory for cleanup: %v", err)
} else {
for _, file := range files {
filePath := filepath.Join(snapsDir, file.Name())
if strings.HasSuffix(file.Name(), ".partial") || strings.HasSuffix(file.Name(), ".delta") {
verboseLog("Removing partial/delta file: %s\n", filePath)
if err := os.Remove(filePath); err != nil {
verboseLog("Failed to remove file %s: %v", filePath, err)
} else if verbose {
verboseLog("Removed partial/delta file: %s", filePath)
}
} else if strings.HasSuffix(file.Name(), ".snap") {
if !validSnaps[file.Name()] {
verboseLog("Removing outdated or orphaned snap file: %s\n", filePath)
if err := os.Remove(filePath); err != nil {
verboseLog("Failed to remove snap file %s: %v", filePath, err)
} else if verbose {
verboseLog("Removed snap file: %s", filePath)
}
} else {
verboseLog("Snap file %s is valid and retained.\n", file.Name())
}
}
}
}
// Remove orphaned assertion files
files, err = os.ReadDir(assertionsDir)
if err != nil {
verboseLog("Error reading assertions directory for cleanup: %v", err)
} else {
for _, file := range files {
filePath := filepath.Join(assertionsDir, file.Name())
if strings.HasSuffix(file.Name(), ".assert") {
if !validAssertions[file.Name()] {
verboseLog("Removing orphaned assertion file: %s\n", filePath)
if err := os.Remove(filePath); err != nil {
verboseLog("Failed to remove assertion file %s: %v", filePath, err)
} else if verbose {
verboseLog("Removed assertion file: %s", filePath)
}
} else {
verboseLog("Assertion file %s is valid and retained.\n", file.Name())
}
}
}
}
verboseLog("Cleanup process completed.")
}
// removeOrphanedFiles deletes the assertion and snap file corresponding to the removed snap.
func removeOrphanedFiles(snapName string, revision int, assertionsDir string, snapsDir string) {
assertionFilePath := filepath.Join(assertionsDir, fmt.Sprintf("%s_%d.assert", snapName, revision))
snapFilePath := filepath.Join(snapsDir, fmt.Sprintf("%s_%d.snap", snapName, revision))
if fileExists(assertionFilePath) {
err := os.Remove(assertionFilePath)
if err != nil {
verboseLog("Failed to remove assertion file %s: %v", assertionFilePath, err)
} else {
verboseLog("Removed assertion file: %s", assertionFilePath)
}
} else {
verboseLog("Assertion file %s does not exist. No action taken.", assertionFilePath)
}
if fileExists(snapFilePath) {
err := os.Remove(snapFilePath)
if err != nil {
verboseLog("Failed to remove snap file %s: %v", snapFilePath, err)
} else {
verboseLog("Removed snap file: %s", snapFilePath)
}
} else {
verboseLog("Snap file %s does not exist. No action taken.", snapFilePath)
}
}
// cleanUpCurrentSnaps removes snaps from currentSnaps that are not marked as required.
func cleanUpCurrentSnaps(assertionsDir string, snapsDir string) {
var filteredSnaps []*store.CurrentSnap
for _, snap := range currentSnaps {
if requiredSnaps[snap.InstanceName] {
filteredSnaps = append(filteredSnaps, snap)
} else {
verboseLog("Removing unnecessary snap: %s\n", snap.InstanceName)
removeOrphanedFiles(snap.InstanceName, snap.Revision.N, assertionsDir, snapsDir)
}
}
currentSnaps = filteredSnaps
// Log the updated currentSnaps
verboseLog("Filtered currentSnaps after cleanup:")
for _, snap := range currentSnaps {
verboseLog("- %s_%d.snap", snap.InstanceName, snap.Revision.N)
}
}
// removeStateJson removes the state.json file if it exists
func removeStateJson(stateJsonPath string) {
if _, err := os.Stat(stateJsonPath); err == nil {
if err := os.Remove(stateJsonPath); err != nil {
verboseLog("Failed to remove state.json: %v", err)
} else if verbose {
verboseLog("Removed state.json at %s", stateJsonPath)
}
}
}

@ -0,0 +1,152 @@
package main
import (
"fmt"
"os/exec"
"path/filepath"
"time"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/store"
)
// downloadSnap downloads a snap file with retry logic
func downloadSnap(storeClient *store.Store, snapInfo *snap.Info, downloadPath string) error {
downloadInfo := &snap.DownloadInfo{
DownloadURL: snapInfo.DownloadURL,
}
pbar := NewProgressMeter(snapInfo.SuggestedName, snapInfo.Version, false)
progressTracker.UpdateStepProgress(0)
for attempts := 1; attempts <= 5; attempts++ {
verboseLog("Attempt %d to download snap: %s", attempts, downloadPath)
err := storeClient.Download(ctx, snapInfo.SnapID, downloadPath, downloadInfo, pbar, nil, nil)
if err == nil {
pbar.Finished()
return nil // Successful download
}
if verbose {
verboseLog("Attempt %d to download %s failed: %v", attempts, snapInfo.SuggestedName, err)
} else if progressTracker != nil {
progressTracker.UpdateStepProgress(0)
}
if attempts == 5 {
return fmt.Errorf("snap download failed after 5 attempts: %v", err)
}
}
return fmt.Errorf("snap download failed after 5 attempts")
}
// downloadSnapDeltaWithRetries downloads the delta file with retry logic and exponential backoff.
func downloadSnapDeltaWithRetries(storeClient *store.Store, delta *snap.DeltaInfo, result *store.SnapActionResult, deltaPath string, maxRetries int, snapName string) error {
if !verbose {
verboseLog("Downloading delta for %s", snapName)
}
var lastErr error
backoff := 1 * time.Second
for attempts := 1; attempts <= maxRetries; attempts++ {
verboseLog("Attempt %d to download delta: %s", attempts, deltaPath)
err := downloadSnapDelta(storeClient, delta, result, deltaPath)
if err == nil {
return nil
}
lastErr = err
verboseLog("Attempt %d to download delta failed: %v", attempts, err)
time.Sleep(backoff)
backoff *= 2
}
return fmt.Errorf("delta download failed after %d attempts: %v", maxRetries, lastErr)
}
// downloadSnapDelta downloads the delta file.
func downloadSnapDelta(storeClient *store.Store, delta *snap.DeltaInfo, result *store.SnapActionResult, deltaPath string) error {
verboseLog("Downloading delta from revision %d to %d from: %s", delta.FromRevision, delta.ToRevision, delta.DownloadURL)
downloadInfo := &snap.DownloadInfo{
DownloadURL: delta.DownloadURL,
Size: delta.Size,
Sha3_384: delta.Sha3_384,
}
// Use the SnapID from the associated SnapActionResult's Info
snapID := result.Info.SnapID
pbar := NewProgressMeter(result.Info.SuggestedName, result.Info.Version, true)
progressTracker.UpdateStepProgress(0)
// Download the delta file
if err := storeClient.Download(ctx, snapID, deltaPath, downloadInfo, pbar, nil, nil); err != nil {
progressTracker.UpdateStepProgress(0)
return fmt.Errorf("delta download failed: %v", err)
}
verboseLog("Downloaded %s to %s", delta.DownloadURL, deltaPath)
pbar.Finished()
return nil
}
// downloadAndApplySnap handles the downloading and delta application process.
// It returns the snap information and an error if any.
func downloadAndApplySnap(storeClient *store.Store, result *store.SnapActionResult, snapsDir, assertionsDir string, currentSnap *store.CurrentSnap) (*snap.Info, error) {
if result == nil || result.Info == nil {
verboseLog("No updates available for snap. Skipping download and assertions.")
return nil, nil
}
snapInfo := result.Info
downloadPath := filepath.Join(snapsDir, fmt.Sprintf("%s_%d.snap", snapInfo.SuggestedName, snapInfo.Revision.N))
// Check if a delta can be applied
if currentSnap != nil && len(result.Deltas) > 0 {
for _, delta := range result.Deltas {
deltaPath := filepath.Join(snapsDir, fmt.Sprintf("%s_%d_to_%d.delta", snapInfo.SuggestedName, delta.FromRevision, delta.ToRevision))
if err := downloadSnapDeltaWithRetries(storeClient, &delta, result, deltaPath, 5, snapInfo.SuggestedName); err == nil {
oldSnapPath := filepath.Join(snapsDir, fmt.Sprintf("%s_%d.snap", snapInfo.SuggestedName, delta.FromRevision))
if fileExists(oldSnapPath) {
if err := applyDelta(oldSnapPath, deltaPath, downloadPath); err == nil {
verboseLog("Delta applied successfully for snap %s", snapInfo.SuggestedName)
// Download assertions after successful snap download
if err := downloadAssertions(storeClient, snapInfo, assertionsDir); err != nil {
return nil, fmt.Errorf("failed to download assertions for snap %s: %w", snapInfo.SuggestedName, err)
}
return snapInfo, nil // Successful delta application
} else {
verboseLog("Failed to apply delta for snap %s: %v", snapInfo.SuggestedName, err)
}
} else {
verboseLog("Old snap file %s does not exist. Cannot apply delta.", oldSnapPath)
}
} else {
verboseLog("Attempt to download delta for snap %s failed: %v", snapInfo.SuggestedName, err)
}
}
}
// If no delta was applied or no deltas are available, fallback to downloading the full snap
if err := downloadSnap(storeClient, snapInfo, downloadPath); err != nil {
return nil, fmt.Errorf("failed to download snap %s: %w", snapInfo.SuggestedName, err)
}
// Download assertions after successful snap download
if err := downloadAssertions(storeClient, snapInfo, assertionsDir); err != nil {
return nil, fmt.Errorf("failed to download assertions for snap %s: %w", snapInfo.SuggestedName, err)
}
verboseLog("Downloaded and applied snap: %s, revision: %d", snapInfo.SuggestedName, snapInfo.Revision.N)
return snapInfo, nil
}
// applyDelta applies the downloaded delta using xdelta3.
func applyDelta(oldSnapPath, deltaPath, newSnapPath string) error {
verboseLog("Applying delta from %s to %s using %s", oldSnapPath, newSnapPath, deltaPath)
cmd := exec.Command("xdelta3", "-d", "-s", oldSnapPath, deltaPath, newSnapPath)
output, err := cmd.CombinedOutput()
if err != nil {
verboseLog("xdelta3 output: %s", string(output))
return fmt.Errorf("failed to apply delta: %v - %s", err, string(output))
}
return nil
}

@ -0,0 +1,31 @@
module snapd-seed-glue
go 1.23.2
require (
github.com/snapcore/snapd v0.0.0-20241012091728-e440fb944764
golang.org/x/crypto v0.28.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/canonical/go-efilib v0.4.0 // indirect
github.com/canonical/go-sp800.108-kdf v0.0.0-20210314145419-a3359f2d21b9 // indirect
github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3 // indirect
github.com/canonical/go-tpm2 v0.0.0-20210827151749-f80ff5afff61 // indirect
github.com/canonical/tcglog-parser v0.0.0-20210824131805-69fa1e9f0ad2 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/juju/ratelimit v1.0.1 // indirect
github.com/snapcore/go-gettext v0.0.0-20191107141714-82bbea49e785 // indirect
github.com/snapcore/secboot v0.0.0-20240411101434-f3ad7c92552a // indirect
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/term v0.25.0 // indirect
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
gopkg.in/macaroon.v1 v1.0.0-20150121114231-ab3940c6c165 // indirect
gopkg.in/retry.v1 v1.0.3 // indirect
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
maze.io/x/crypto v0.0.0-20190131090603-9b94c9afe066 // indirect
)

@ -0,0 +1,44 @@
github.com/canonical/go-efilib v0.4.0 h1:2ee5pvhIZ+g1EO4HxFE/owBgs5Up2g7dw1+Ls9/fiSs=
github.com/canonical/go-efilib v0.4.0/go.mod h1:9b2PNAuPcZsB76x75/uwH99D8CyH/A2y4rq1/+bvplg=
github.com/canonical/go-sp800.108-kdf v0.0.0-20210314145419-a3359f2d21b9 h1:USzKjrfWo/ESzozv2i3OMM7XDgxrZRvaHFrKkIKRtwU=
github.com/canonical/go-sp800.108-kdf v0.0.0-20210314145419-a3359f2d21b9/go.mod h1:Zrs3YjJr+w51u0R/dyLh/oWt/EcBVdLPCVFYC4daW5s=
github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3 h1:oe6fCvaEpkhyW3qAicT0TnGtyht/UrgvOwMcEgLb7Aw=
github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3/go.mod h1:qdP0gaj0QtgX2RUZhnlVrceJ+Qln8aSlDyJwelLLFeM=
github.com/canonical/go-tpm2 v0.0.0-20210827151749-f80ff5afff61 h1:DsyeCtFXqOdukmhPOunohjSlyxDHTqWSW1O4rD9N3L8=
github.com/canonical/go-tpm2 v0.0.0-20210827151749-f80ff5afff61/go.mod h1:vG41hdbBjV4+/fkubTT1ENBBqSkLwLr7mCeW9Y6kpZY=
github.com/canonical/tcglog-parser v0.0.0-20210824131805-69fa1e9f0ad2 h1:CbwVq64ruNLx/S3XA0LO6QMsw6Vc2inK+RcS6D2c4Ns=
github.com/canonical/tcglog-parser v0.0.0-20210824131805-69fa1e9f0ad2/go.mod h1:QoW2apR2tBl6T/4czdND/EHjL1Ia9cCmQnIj9Xe0Kt8=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
github.com/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY=
github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/snapcore/go-gettext v0.0.0-20191107141714-82bbea49e785 h1:PaunR+BhraKSLxt2awQ42zofkP+NKh/VjQ0PjIMk/y4=
github.com/snapcore/go-gettext v0.0.0-20191107141714-82bbea49e785/go.mod h1:D3SsWAXK7wCCBZu+Vk5hc1EuKj/L3XN1puEMXTU4LrQ=
github.com/snapcore/secboot v0.0.0-20240411101434-f3ad7c92552a h1:yzzVi0yUosDYkjSQqGZNVtaVi+6yNFLiF0erKHlBbdo=
github.com/snapcore/secboot v0.0.0-20240411101434-f3ad7c92552a/go.mod h1:72paVOkm4sJugXt+v9ItmnjXgO921D8xqsbH2OekouY=
github.com/snapcore/snapd v0.0.0-20241012091728-e440fb944764 h1:RBbGB8WMdKgB7FCPXuUIBns8L+LezNsbvYNYXrG752w=
github.com/snapcore/snapd v0.0.0-20241012091728-e440fb944764/go.mod h1:c9j4npxTEGjMy6E/7ocAazCMR+82ZWTh/o+5Ic7ilPc=
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 h1:A/5uWzF44DlIgdm/PQFwfMkW0JX+cIcQi/SwLAmZP5M=
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0=
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
gopkg.in/macaroon.v1 v1.0.0-20150121114231-ab3940c6c165 h1:85xqOSyTpSzplW7fyO9bOZpSsemJc9UKzEQR2L4k32k=
gopkg.in/macaroon.v1 v1.0.0-20150121114231-ab3940c6c165/go.mod h1:PABpHZvxAbIuSYTPWJdQsNu0mtx+HX/1NIm3IT95IX0=
gopkg.in/retry.v1 v1.0.3 h1:a9CArYczAVv6Qs6VGoLMio99GEs7kY9UzSF9+LD+iGs=
gopkg.in/retry.v1 v1.0.3/go.mod h1:FJkXmWiMaAo7xB+xhvDF59zhfjDWyzmyAxiT4dB688g=
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV4fzYhNBql77zY0ykqs=
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maze.io/x/crypto v0.0.0-20190131090603-9b94c9afe066 h1:UrD21H1Ue5Nl8f2x/NQJBRdc49YGmla3mRStinH8CCE=
maze.io/x/crypto v0.0.0-20190131090603-9b94c9afe066/go.mod h1:DEvumi+swYmlKxSlnsvPwS15tRjoypCCeJFXswU5FfQ=

@ -0,0 +1,202 @@
package main
import (
"context"
"flag"
"log"
"fmt"
"path/filepath"
"strings"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/store"
)
// Seed structure for seed.yaml
type seed struct {
Snaps []struct {
Name string `yaml:"name"`
Channel string `yaml:"channel"`
File string `yaml:"file"`
} `yaml:"snaps"`
}
var (
ctx = context.Background()
storeClient *store.Store
verbose bool
currentSnaps []*store.CurrentSnap
requiredSnaps map[string]bool
processedSnaps = make(map[string]bool)
snapSizeMap = make(map[string]float64)
totalSnapSize float64
)
type SnapInfo struct {
InstanceName string
SnapID string
Revision snap.Revision
}
func main() {
// Override the default plug slot sanitizer
snap.SanitizePlugsSlots = sanitizePlugsSlots
// Initialize progress reporting
InitProgress()
totalSnapSize = 0
// Initialize the store client
storeClient = store.New(nil, nil)
// Parse command-line flags
var seedDirectory string
flag.StringVar(&seedDirectory, "seed", "/var/lib/snapd/seed", "Specify the seed directory")
flag.BoolVar(&verbose, "verbose", false, "Enable verbose output")
flag.Parse()
if !verbose {
fmt.Printf("2\tLoading existing snaps...\n")
}
// Define directories based on the seed directory
snapsDir := filepath.Join(seedDirectory, "snaps")
assertionsDir := filepath.Join(seedDirectory, "assertions")
seedYaml := filepath.Join(seedDirectory, "seed.yaml")
// Setup directories and seed.yaml
initializeDirectories(snapsDir, assertionsDir)
initializeSeedYaml(seedYaml)
// Load existing snaps from seed.yaml
existingSnapsInYaml := loadExistingSnaps(seedYaml)
// Populate currentSnaps based on existing snaps
for snapName := range existingSnapsInYaml {
snapInfo, err := getCurrentSnapInfo(assertionsDir, snapName)
if err != nil {
verboseLog("Failed to get info for existing snap %s: %v", snapName, err)
continue
}
currentSnaps = append(currentSnaps, snapInfo)
}
// Process essential snaps
requiredSnaps = map[string]bool{"snapd": true, "bare": true}
for _, arg := range flag.Args() {
requiredSnaps[arg] = true
}
if !verbose {
fmt.Printf("4\tFetching information from the Snap Store...\n")
}
// Collect snaps to process
snapsToProcess, err := collectSnapsToProcess(snapsDir, assertionsDir)
if err != nil {
log.Fatalf("Failed to collect snaps to process: %v", err)
}
progressTracker.Finish("Finished collecting snap info")
// Calculate the number of snaps to download
totalSnaps := len(snapsToProcess)
if totalSnaps == 0 {
verboseLog("No snaps to process.")
} else {
verboseLog("Total snaps to download: %d", totalSnaps)
}
// Initialize variables to track download progress
completedSnaps := 0
// Update "Downloading snaps" step to 0%
progressTracker.UpdateStepProgress(0)
// Process all the snaps that need updates
for _, snapDetails := range snapsToProcess {
if err := processSnap(snapDetails, snapsDir, assertionsDir); err != nil {
log.Fatalf("Failed to process snap %s: %v", snapDetails.InstanceName, err)
}
completedSnaps++
progressTracker.UpdateStepProgress(-1)
}
// Mark "Downloading snaps" as complete
if totalSnaps > 0 {
progressTracker.Finish("Downloading snaps completed")
} else {
// If no snaps to download, skip to finalizing
progressTracker.NextStep()
}
// Remove unnecessary snaps after processing dependencies
cleanUpCurrentSnaps(assertionsDir, snapsDir)
// Update seed.yaml with the current required snaps
if err := updateSeedYaml(snapsDir, seedYaml, currentSnaps); err != nil {
log.Fatalf("Failed to update seed.yaml: %v", err)
}
// Perform cleanup and validation tasks
removeStateJson(filepath.Join(seedDirectory, "..", "state.json"))
ensureAssertions(assertionsDir)
if err := validateSeed(seedYaml); err != nil {
log.Fatalf("Seed validation failed: %v", err)
}
cleanUpFiles(snapsDir, assertionsDir, seedYaml)
// Mark "Finalizing" as complete
if progressTracker != nil {
progressTracker.Finish("Cleanup and validation completed")
}
}
// collectSnapsToProcess collects all snaps and their dependencies, returning only those that need updates
func collectSnapsToProcess(snapsDir, assertionsDir string) ([]SnapDetails, error) {
var snapsToProcess []SnapDetails
for snapEntry := range requiredSnaps {
// Extract channel if specified, default to "stable"
parts := strings.SplitN(snapEntry, "=", 2)
channel := "stable"
if len(parts) == 2 {
channel = parts[1]
}
snapName := parts[0]
// Collect snap dependencies and their statuses
snapList, err := collectSnapDependencies(snapName, channel, snapsDir, assertionsDir)
if err != nil {
return nil, err
}
// Append only those snaps that need updates
for _, snapDetails := range snapList {
verboseLog("Processing snap: %s with result: %v", snapDetails.InstanceName, snapDetails.Result)
if len(snapDetails.Result.Deltas) > 0 {
for _, delta := range snapDetails.Result.Deltas {
snapSize := float64(delta.Size)
snapSizeMap[snapDetails.Result.Info.SuggestedName] = snapSize
totalSnapSize += snapSize
}
} else {
snapSize := float64(snapDetails.Result.Info.Size)
snapSizeMap[snapDetails.Result.Info.SuggestedName] = snapSize
totalSnapSize += snapSize
}
snapsToProcess = append(snapsToProcess, snapDetails)
}
}
return snapsToProcess, nil
}
// sanitizePlugsSlots is a placeholder function to sanitize plug slots in snap.Info
func sanitizePlugsSlots(info *snap.Info) {}
// verboseLog logs messages only when verbose mode is enabled
func verboseLog(format string, v ...interface{}) {
if verbose {
log.Printf(format, v...)
}
}

@ -0,0 +1,220 @@
package main
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/store"
)
// SnapDetails struct remains unchanged
type SnapDetails struct {
InstanceName string
Channel string
CurrentSnap *store.CurrentSnap
Result *store.SnapActionResult
}
// collectSnapDependencies collects all dependencies for a given snap, marking them as requiredSnaps regardless of whether they need updates.
func collectSnapDependencies(snapName, channel, snapsDir, assertionsDir string) ([]SnapDetails, error) {
var snapDetailsList []SnapDetails
if processedSnaps[snapName] {
verboseLog("Snap %s has already been processed. Skipping.", snapName)
return snapDetailsList, nil // Added 0 for int64
}
oldSnapPath, oldSnap := findPreviousSnap(snapsDir, assertionsDir, snapName)
var result *store.SnapActionResult
var err error
// Fetch or refresh snap information
if oldSnap == nil || oldSnap.SnapID == "" || oldSnap.Revision.N == 0 {
result, err = fetchOrRefreshSnapInfo(snapName, nil, channel)
if err != nil {
return nil, err // Added 0 for int64
}
} else {
result, err = fetchOrRefreshSnapInfo(snapName, oldSnap, channel)
if err != nil && strings.Contains(err.Error(), "snap has no updates available") {
result, err = fetchOrRefreshSnapInfo(snapName, nil, channel)
if err != nil {
return nil, err // Added 0 for int64
}
}
}
if result == nil || result.Info == nil || result.Info.SnapID == "" || result.Info.Revision.N == 0 {
return nil, fmt.Errorf("invalid snap information returned for %s: SnapID or Revision is missing", snapName) // Added 0 for int64
}
info := result.Info
newSnap := &store.CurrentSnap{
InstanceName: snapName,
SnapID: info.SnapID,
Revision: snap.Revision{N: info.Revision.N},
}
snapInCurrentSnaps, oldRevision := isSnapInCurrentSnaps(snapName)
if snapInCurrentSnaps {
removeSnapFromCurrentSnaps(snapName, oldRevision)
}
currentSnaps = append(currentSnaps, newSnap)
processedSnaps[snapName] = true
needsUpdate := (oldSnapPath == "" || oldSnap.Revision.N < info.Revision.N)
if needsUpdate {
snapDetailsList = append(snapDetailsList, SnapDetails{
InstanceName: snapName,
Channel: channel,
CurrentSnap: newSnap,
Result: result,
})
} else {
// Mark the snap as required even if no update is needed
requiredSnaps[snapName] = true
}
// Safely handle dependencies
tracker := snap.SimplePrereqTracker{}
missingPrereqs := tracker.MissingProviderContentTags(info, nil)
for prereq := range missingPrereqs {
if !processedSnaps[prereq] {
verboseLog("Collecting dependencies for prerequisite snap: %s for %s", prereq, snapName)
prereqDetails, err := collectSnapDependencies(prereq, "stable", snapsDir, assertionsDir)
if err != nil {
// Additional logging for dependency resolution issues
verboseLog("Failed to collect dependencies for prerequisite %s for snap %s: %v", prereq, snapName, err)
return nil, fmt.Errorf("failed to collect dependencies for prerequisite %s for snap %s: %v", prereq, snapName, err) // Added 0 for int64
}
snapDetailsList = append(snapDetailsList, prereqDetails...)
}
}
// Also handle base snaps safely
if info.Base != "" && !processedSnaps[info.Base] {
verboseLog("Collecting dependencies for base snap: %s for %s", info.Base, snapName)
baseDetails, err := collectSnapDependencies(info.Base, "stable", snapsDir, assertionsDir)
if err != nil {
verboseLog("Failed to collect dependencies for base snap %s for snap %s: %v", info.Base, snapName, err)
return nil, fmt.Errorf("failed to collect dependencies for base snap %s for snap %s: %v", info.Base, snapName, err) // Added 0 for int64
}
snapDetailsList = append(snapDetailsList, baseDetails...)
}
return snapDetailsList, nil
}
// processSnap handles the downloading and applying of a snap if updates are available.
// It gracefully handles the "no updates available" scenario and ensures dependencies are marked as required.
func processSnap(snapDetails SnapDetails, snapsDir, assertionsDir string) error {
verboseLog("Processing snap: %s on channel: %s", snapDetails.InstanceName, snapDetails.Channel)
// Proceed with downloading the snap (either full or delta) using downloadAndApplySnap
snapInfo, err := downloadAndApplySnap(storeClient, snapDetails.Result, snapsDir, assertionsDir, snapDetails.CurrentSnap)
if err != nil {
return fmt.Errorf("failed to download snap %s: %w", snapDetails.InstanceName, err)
}
// Mark the snap as required after successful download and application
requiredSnaps[snapDetails.InstanceName] = true
verboseLog("Downloaded and applied snap: %s, revision: %d", snapInfo.SuggestedName, snapInfo.Revision.N)
return nil
}
// fetchOrRefreshSnapInfo retrieves snap information and returns the SnapActionResult including deltas.
func fetchOrRefreshSnapInfo(snapName string, currentSnap *store.CurrentSnap, channel string) (*store.SnapActionResult, error) {
var actions []*store.SnapAction
var includeSnap []*store.CurrentSnap
if currentSnap != nil {
verboseLog("Crafting refresh SnapAction for %s", snapName)
actions = append(actions, &store.SnapAction{
Action: "refresh",
SnapID: currentSnap.SnapID,
InstanceName: snapName,
Channel: channel,
})
includeSnap = []*store.CurrentSnap{currentSnap}
} else {
verboseLog("Crafting install SnapAction for %s", snapName)
actions = append(actions, &store.SnapAction{
Action: "install",
InstanceName: snapName,
Channel: channel,
})
includeSnap = []*store.CurrentSnap{}
}
results, _, err := storeClient.SnapAction(ctx, includeSnap, actions, nil, nil, nil)
if err != nil {
verboseLog("SnapAction error for %s: %v", snapName, err)
if strings.Contains(err.Error(), "snap has no updates available") && currentSnap != nil {
return nil, err
}
return nil, fmt.Errorf("snap action failed for %s: %w", snapName, err)
}
if len(results) == 0 || results[0].Info == nil {
return nil, fmt.Errorf("no snap info returned for snap %s", snapName)
}
result := &results[0]
info := result.Info
// Validate necessary fields in the snap information
if info.SnapID == "" || info.Revision.N == 0 {
return nil, fmt.Errorf("invalid snap information for %s: SnapID or Revision is missing", snapName)
}
verboseLog("Fetched latest snap info for %s: SnapID: %s, Revision: %d", snapName, info.SnapID, info.Revision.N)
return result, nil
}
// findPreviousSnap locates the previous snap revision in the downloads directory.
func findPreviousSnap(downloadDir, assertionsDir, snapName string) (string, *store.CurrentSnap) {
var currentSnap store.CurrentSnap
files, err := os.ReadDir(downloadDir)
if err != nil {
verboseLog("Error reading directory: %v", err)
return "", nil
}
var latestRevision int
var latestSnapPath string
for _, file := range files {
if strings.HasPrefix(file.Name(), snapName+"_") && strings.HasSuffix(file.Name(), ".snap") {
revisionStr := extractRevisionFromFile(file.Name())
if revisionStr == "" {
verboseLog("Failed to extract revision from file name: %s", file.Name())
continue
}
revision, err := strconv.Atoi(revisionStr)
if err != nil {
verboseLog("Failed to parse revision number for file %s: %v", file.Name(), err)
continue
}
verboseLog("Found %s with revision %d", file.Name(), revision)
if revision > latestRevision {
latestRevision = revision
latestSnapPath = filepath.Join(downloadDir, file.Name())
// Parse the corresponding assertion file
assertFilePath := filepath.Join(assertionsDir, strings.Replace(file.Name(), ".snap", ".assert", 1))
currentSnap = parseSnapInfo(assertFilePath, snapName)
currentSnap.Revision.N = revision
}
}
}
if latestSnapPath != "" {
return latestSnapPath, &currentSnap
}
return "", nil
}

@ -0,0 +1,286 @@
package main
import (
"fmt"
"sync"
"github.com/snapcore/snapd/progress"
)
var (
progressReporter ProgressReporter
progressTracker *ProgressTracker
globalDownloaded float64
globalMu sync.Mutex
lastReported int
)
type ProgressReporter interface {
Report(percentage int, status string)
}
// VerboseProgressReporter formats progress updates
type VerboseProgressReporter struct{}
func (v *VerboseProgressReporter) Report(percentage int, status string) {
fmt.Printf("%d\t%s\n", percentage, status)
}
// ProgressMeter tracks the download progress and implements the progress.Meter interface
type ProgressMeter struct {
currentBytes float64
isDelta bool
mu sync.Mutex
snapName string
snapVersion string
totalSize float64
}
// Ensure ProgressMeter implements the progress.Meter interface
var _ progress.Meter = (*ProgressMeter)(nil)
// NewProgressMeter initializes a new ProgressMeter instance with the snap name
func NewProgressMeter(snapName string, snapVersion string, isDelta bool) *ProgressMeter {
return &ProgressMeter{
isDelta: isDelta,
snapName: snapName,
snapVersion: snapVersion,
totalSize: snapSizeMap[snapName],
}
}
// Start initializes the progress meter with the total size
func (pm *ProgressMeter) Start(label string, total float64) {
pm.mu.Lock()
defer pm.mu.Unlock()
pm.totalSize = total
// Update global total size
globalMu.Lock()
globalMu.Unlock()
}
// Set updates the progress based on the current value representing the number of bytes downloaded
func (pm *ProgressMeter) Set(value float64) {
pm.mu.Lock()
defer pm.mu.Unlock()
delta := value - pm.currentBytes
pm.currentBytes = value
// Update global downloaded bytes
globalMu.Lock()
globalDownloaded += delta
globalMu.Unlock()
reportGlobalProgress(pm.snapName, pm.snapVersion, pm.isDelta)
}
// SetTotal sets the total size for the ProgressMeter
func (pm *ProgressMeter) SetTotal(total float64) {
pm.mu.Lock()
defer pm.mu.Unlock()
pm.totalSize = total
}
// Finished marks the progress as complete
func (pm *ProgressMeter) Finished() {
pm.mu.Lock()
defer pm.mu.Unlock()
delta := pm.totalSize - pm.currentBytes
pm.currentBytes = pm.totalSize
// Update global downloaded bytes
globalMu.Lock()
globalDownloaded += delta
globalMu.Unlock()
reportGlobalProgress(pm.snapName, pm.snapVersion, pm.isDelta)
}
// Write handles byte data to update progress based on the size of the data written
func (pm *ProgressMeter) Write(p []byte) (n int, err error) {
pm.mu.Lock()
defer pm.mu.Unlock()
// Calculate the delta and update the current bytes
delta := float64(len(p))
pm.currentBytes += delta
// Update global downloaded bytes
globalMu.Lock()
globalDownloaded += delta
globalMu.Unlock()
reportGlobalProgress(pm.snapName, pm.snapVersion, pm.isDelta)
return len(p), nil
}
// reportGlobalProgress calculates and formats the overall progress percentage
func reportGlobalProgress(snapName string, snapVersion string, isDelta bool) {
globalMu.Lock()
defer globalMu.Unlock()
if totalSnapSize == 0 {
return
}
// Calculate the percentage within the range of 10 to 90
percentage := int((globalDownloaded / totalSnapSize) * 80) + 10
// Only print if there's a change in percentage to reduce output
if percentage != lastReported {
lastReported = percentage
if isDelta {
progressString := fmt.Sprintf("Downloading delta for snap %s %s", snapName, snapVersion)
progressReporter.Report(percentage, progressString)
} else {
progressString := fmt.Sprintf("Downloading snap %s %s", snapName, snapVersion)
progressReporter.Report(percentage, progressString)
}
}
}
// Spin shows indefinite activity; not used in this implementation
func (pm *ProgressMeter) Spin(msg string) {
fmt.Printf("Spin: %s\n", msg)
}
// Notify formats notifications about the progress
func (pm *ProgressMeter) Notify(message string) {
fmt.Printf("Notification: %s\n", message)
}
// ProgressTracker manages multiple steps of progress
type ProgressTracker struct {
totalWeight int
completedWeight float64
mu sync.Mutex
reporter ProgressReporter
steps []*WeightedStep
currentStep int
}
// WeightedStep represents a step in a multi-step progress tracker
type WeightedStep struct {
Weight int
Progress float64
Status string
}
// NewProgressTracker creates a new instance of ProgressTracker
func NewProgressTracker(reporter ProgressReporter) *ProgressTracker {
return &ProgressTracker{
reporter: reporter,
steps: []*WeightedStep{},
}
}
// AddStep adds a new step to the progress tracker
func (pt *ProgressTracker) AddStep(weight int, status string) {
pt.mu.Lock()
defer pt.mu.Unlock()
pt.steps = append(pt.steps, &WeightedStep{
Weight: weight,
Status: status,
})
pt.totalWeight += weight
}
// Start initializes the first step of the tracker
func (pt *ProgressTracker) Start() {
pt.mu.Lock()
defer pt.mu.Unlock()
if len(pt.steps) > 0 {
pt.currentStep = 0
pt.steps[pt.currentStep].Progress = 0
pt.reportProgress()
}
}
// UpdateStepProgress updates the current step's progress
func (pt *ProgressTracker) UpdateStepProgress(progress float64) {
pt.mu.Lock()
defer pt.mu.Unlock()
if pt.currentStep >= len(pt.steps) {
return
}
step := pt.steps[pt.currentStep]
if progress < 0 {
progress = float64(int((globalDownloaded / totalSnapSize) * float64(step.Weight)))
} else if progress < step.Progress {
return
}
// Adjust delta calculation
delta := (progress - step.Progress)
pt.completedWeight += delta
step.Progress = progress
//pt.reportProgress()
}
func (pt *ProgressTracker) Finish(status string) {
pt.mu.Lock()
defer pt.mu.Unlock()
if len(pt.steps) == 0 || pt.currentStep >= len(pt.steps) {
return
}
step := pt.steps[pt.currentStep]
if step.Progress < 100.0 {
// Corrected calculation to prevent exceeding 100%
remainingProgress := 100.0 - step.Progress
delta := (remainingProgress * float64(step.Weight)) / 100.0
pt.completedWeight += delta
step.Progress = 100.0
}
percentage := pt.calculatePercentage()
pt.reporter.Report(percentage, status)
if pt.currentStep < len(pt.steps)-1 {
pt.currentStep++
pt.steps[pt.currentStep].Progress = 0
pt.reportProgress()
}
}
// NextStep moves to the next step in the progress tracker
func (pt *ProgressTracker) NextStep() {
pt.mu.Lock()
defer pt.mu.Unlock()
if pt.currentStep < len(pt.steps)-1 {
pt.currentStep++
pt.steps[pt.currentStep].Progress = 0
pt.reportProgress()
}
}
// calculatePercentage calculates the overall progress as a percentage
func (pt *ProgressTracker) calculatePercentage() int {
if pt.totalWeight == 0 {
return 0
}
// Calculate percentage accurately and scale within 0-99 range
percentage := int((pt.completedWeight / float64(pt.totalWeight)) * 100)
if percentage > 99 {
percentage = 99
}
return percentage
}
// reportProgress reports the current progress percentage to the reporter
func (pt *ProgressTracker) reportProgress() {
percentage := pt.calculatePercentage()
status := pt.steps[pt.currentStep].Status
if percentage != lastReported {
pt.reporter.Report(percentage, status)
lastReported = percentage
}
}
// InitProgress initializes the global progress tracker and sets up steps
func InitProgress() {
progressReporter = &VerboseProgressReporter{}
progressTracker = NewProgressTracker(progressReporter)
progressTracker.AddStep(10, "Initialization")
progressTracker.AddStep(80, "Downloading snaps")
progressTracker.AddStep(10, "Verifying snaps")
progressTracker.Start()
}

@ -0,0 +1,114 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"os"
"gopkg.in/yaml.v3"
"github.com/snapcore/snapd/store"
)
// initializeSeedYaml ensures that seed.yaml exists; if not, creates it.
func initializeSeedYaml(seedYaml string) {
if _, err := os.Stat(seedYaml); os.IsNotExist(err) {
file, err := os.Create(seedYaml)
if err != nil {
log.Fatalf("Failed to create seed.yaml: %v", err)
}
defer file.Close()
file.WriteString("snaps:\n")
}
}
// loadSeedData loads seed data from seed.yaml
func loadSeedData(seedYaml string) seed {
file, err := ioutil.ReadFile(seedYaml)
if err != nil {
log.Fatalf("Failed to read seed.yaml: %v", err)
}
var seedData seed
if err := yaml.Unmarshal(file, &seedData); err != nil {
log.Fatalf("Failed to parse seed.yaml: %v", err)
}
return seedData
}
// loadExistingSnaps loads snaps from seed.yaml into a map
func loadExistingSnaps(seedYaml string) map[string]bool {
file, err := ioutil.ReadFile(seedYaml)
if err != nil {
log.Fatalf("Failed to read seed.yaml: %v", err)
}
var seedData seed
if err := yaml.Unmarshal(file, &seedData); err != nil {
log.Fatalf("Failed to parse seed.yaml: %v", err)
}
existing := make(map[string]bool)
for _, snap := range seedData.Snaps {
existing[snap.Name] = true
verboseLog("Found %s in seed.yaml\n", snap.Name)
}
return existing
}
// updateSeedYaml updates the seed.yaml file with the current required snaps
func updateSeedYaml(snapsDir, seedYaml string, currentSnaps []*store.CurrentSnap) error {
// Log the snaps to be written
verboseLog("CurrentSnaps to be written to seed.yaml:")
for _, snapInfo := range currentSnaps {
verboseLog("- %s_%d.snap", snapInfo.InstanceName, snapInfo.Revision.N)
}
// Load existing seed data
file, err := ioutil.ReadFile(seedYaml)
if err != nil {
return fmt.Errorf("failed to read seed.yaml: %w", err)
}
var seedData seed
if err := yaml.Unmarshal(file, &seedData); err != nil {
return fmt.Errorf("failed to parse seed.yaml: %w", err)
}
// Clear existing snaps
seedData.Snaps = []struct {
Name string `yaml:"name"`
Channel string `yaml:"channel"`
File string `yaml:"file"`
}{}
// Populate seedData with currentSnaps
for _, snapInfo := range currentSnaps {
snapFileName := fmt.Sprintf("%s_%d.snap", snapInfo.InstanceName, snapInfo.Revision.N)
snapData := struct {
Name string `yaml:"name"`
Channel string `yaml:"channel"`
File string `yaml:"file"`
}{
Name: snapInfo.InstanceName,
Channel: "stable", // Assuming 'stable' channel; modify as needed
File: snapFileName,
}
seedData.Snaps = append(seedData.Snaps, snapData)
}
// Marshal the updated seedData back to YAML
updatedYAML, err := yaml.Marshal(&seedData)
if err != nil {
return fmt.Errorf("failed to marshal updated seed data: %w", err)
}
// Write the updated YAML back to seed.yaml
if err := ioutil.WriteFile(seedYaml, updatedYAML, 0644); err != nil {
return fmt.Errorf("failed to write updated seed.yaml: %w", err)
}
verboseLog("Updated seed.yaml with current snaps.")
return nil
}

@ -0,0 +1,150 @@
package main
import (
"bufio"
"encoding/hex"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/store"
"golang.org/x/crypto/sha3"
)
// verifyChecksum calculates the SHA3-384 checksum of a file and compares it with the expected checksum.
func verifyChecksum(filePath, expectedChecksum string) (bool, error) {
file, err := os.Open(filePath)
if err != nil {
return false, fmt.Errorf("failed to open file for checksum verification: %w", err)
}
defer file.Close()
hash := sha3.New384()
if _, err := io.Copy(hash, file); err != nil {
return false, fmt.Errorf("failed to calculate checksum: %w", err)
}
calculatedChecksum := hex.EncodeToString(hash.Sum(nil))
return strings.EqualFold(calculatedChecksum, expectedChecksum), nil
}
// extractRevisionFromFile extracts the revision number from a file name by splitting at the last underscore.
func extractRevisionFromFile(fileName string) string {
lastUnderscore := strings.LastIndex(fileName, "_")
if lastUnderscore == -1 {
return ""
}
revisionWithSuffix := fileName[lastUnderscore+1:]
// Handle both .snap and .assert suffixes
if strings.HasSuffix(revisionWithSuffix, ".snap") {
return strings.TrimSuffix(revisionWithSuffix, ".snap")
} else if strings.HasSuffix(revisionWithSuffix, ".assert") {
return strings.TrimSuffix(revisionWithSuffix, ".assert")
}
return revisionWithSuffix
}
// fileExists checks if a file exists and is not a directory before we use it
func fileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
// parseSnapInfo extracts snap details from the assertion file and returns the snap's current revision.
func parseSnapInfo(assertFilePath, snapName string) store.CurrentSnap {
currentSnap := store.CurrentSnap{InstanceName: snapName}
file, err := os.Open(assertFilePath)
if err != nil {
verboseLog("Failed to read assertion file: %v\n", err)
return currentSnap
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "snap-id:") {
currentSnap.SnapID = strings.TrimSpace(strings.SplitN(line, ":", 2)[1])
}
if strings.HasPrefix(line, "snap-revision:") {
revisionStr := strings.TrimSpace(strings.SplitN(line, ":", 2)[1])
revision, err := strconv.Atoi(revisionStr)
if err != nil {
verboseLog("Failed to parse snap-revision for snap %s: %v", snapName, err)
continue
}
currentSnap.Revision.N = revision
}
}
// Validate that required fields are present
if currentSnap.SnapID == "" || currentSnap.Revision.N == 0 {
verboseLog("Incomplete snap info in assertion file: %s", assertFilePath)
}
return currentSnap
}
// isSnapInCurrentSnaps checks if a snap is already in currentSnaps
func isSnapInCurrentSnaps(snapName string) (bool, snap.Revision) {
for _, snap := range currentSnaps {
if snap.InstanceName == snapName {
return true, snap.Revision
}
}
return false, snap.Revision{}
}
func removeSnapFromCurrentSnaps(snapName string, revision snap.Revision) {
for i := 0; i < len(currentSnaps); i++ {
if currentSnaps[i].Revision == revision {
// Remove the element at index i
currentSnaps = append(currentSnaps[:i], currentSnaps[i+1:]...)
return
}
}
}
// initializeDirectories ensures that the snaps and assertions directories exist
func initializeDirectories(snapsDir, assertionsDir string) {
if err := os.MkdirAll(snapsDir, 0755); err != nil {
log.Fatalf("Failed to create snaps directory: %v", err)
}
if err := os.MkdirAll(assertionsDir, 0755); err != nil {
log.Fatalf("Failed to create assertions directory: %v", err)
}
}
// getCurrentSnapInfo retrieves current snap information from assertions
func getCurrentSnapInfo(assertionsDir, snapName string) (*store.CurrentSnap, error) {
assertionFiles, err := filepath.Glob(filepath.Join(assertionsDir, fmt.Sprintf("%s_*.assert", snapName)))
if err != nil || len(assertionFiles) == 0 {
return nil, fmt.Errorf("no assertion file found for snap: %s", snapName)
}
assertionFile := assertionFiles[0]
currentSnap := parseSnapInfo(assertionFile, snapName)
if currentSnap.SnapID == "" || currentSnap.Revision.N == 0 {
return nil, fmt.Errorf("incomplete snap info in assertion file for snap: %s", snapName)
}
verboseLog("Found snap info for %s: SnapID: %s, Revision: %d", snapName, currentSnap.SnapID, currentSnap.Revision.N)
return &currentSnap, nil
}
// verifySnapIntegrity verifies the integrity of a snap file
func verifySnapIntegrity(filePath, expectedChecksum string) bool {
checksumMatches, err := verifyChecksum(filePath, expectedChecksum)
if err != nil {
verboseLog("Checksum verification failed for %s: %v", filePath, err)
return false
}
return checksumMatches
}

@ -0,0 +1,108 @@
package main
import (
"encoding/base64"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
)
// validateSeed validates the seed using snap debug
func validateSeed(seedYaml string) error {
cmd := exec.Command("snap", "debug", "validate-seed", seedYaml)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("validation failed with output: %s, error: %v", string(output), err)
}
verboseLog("Seed validation successful: %s", string(output))
return nil
}
// ensureAssertions ensures that essential assertions are present
func ensureAssertions(assertionsDir string) {
model := "generic-classic"
brand := "generic"
series := "16" // Hardcoded series as snap.Info does not have a Series field
modelAssertionPath := filepath.Join(assertionsDir, "model")
accountKeyAssertionPath := filepath.Join(assertionsDir, "account-key")
accountAssertionPath := filepath.Join(assertionsDir, "account")
// Check and generate model assertion
if _, err := os.Stat(modelAssertionPath); os.IsNotExist(err) {
output, err := exec.Command("snap", "known", "--remote", "model", "series="+series, "model="+model, "brand-id="+brand).CombinedOutput()
if err != nil {
log.Fatalf("Failed to fetch model assertion: %v, Output: %s", err, string(output))
}
if err := ioutil.WriteFile(modelAssertionPath, output, 0644); err != nil {
log.Fatalf("Failed to write model assertion: %v", err)
}
verboseLog("Fetched and saved model assertion to %s", modelAssertionPath)
}
// Generate account-key assertion if not exists
if _, err := os.Stat(accountKeyAssertionPath); os.IsNotExist(err) {
signKeySha3 := grepPattern(modelAssertionPath, "sign-key-sha3-384: ")
if signKeySha3 == "" {
log.Fatalf("Failed to extract sign-key-sha3-384 from model assertion.")
}
decodedSignKey, err := base64.StdEncoding.DecodeString(signKeySha3)
if err != nil {
log.Fatalf("Failed to decode sign-key-sha3-384: %v", err)
}
encodedSignKey := base64.StdEncoding.EncodeToString(decodedSignKey)
output, err := exec.Command("snap", "known", "--remote", "account-key", "public-key-sha3-384="+encodedSignKey).CombinedOutput()
if err != nil {
log.Fatalf("Failed to fetch account-key assertion: %v, Output: %s", err, string(output))
}
if err := ioutil.WriteFile(accountKeyAssertionPath, output, 0644); err != nil {
log.Fatalf("Failed to write account-key assertion: %v", err)
}
verboseLog("Fetched and saved account-key assertion to %s", accountKeyAssertionPath)
}
// Generate account assertion if not exists
if _, err := os.Stat(accountAssertionPath); os.IsNotExist(err) {
accountId := grepPattern(accountKeyAssertionPath, "account-id: ")
if accountId == "" {
log.Fatalf("Failed to extract account-id from account-key assertion.")
}
output, err := exec.Command("snap", "known", "--remote", "account", "account-id="+accountId).CombinedOutput()
if err != nil {
log.Fatalf("Failed to fetch account assertion: %v, Output: %s", err, string(output))
}
if err := ioutil.WriteFile(accountAssertionPath, output, 0644); err != nil {
log.Fatalf("Failed to write account assertion: %v", err)
}
verboseLog("Fetched and saved account assertion to %s", accountAssertionPath)
}
}
// grepPattern extracts a specific pattern from a file
func grepPattern(filePath, pattern string) string {
content, err := ioutil.ReadFile(filePath)
if err != nil {
log.Fatalf("Failed to read from file %s: %v", filePath, err)
}
lines := strings.Split(string(content), "\n")
for _, line := range lines {
if strings.Contains(line, pattern) {
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
encodedValue := strings.TrimSpace(parts[1])
// Check if the value is base64 encoded
if decodedBytes, err := base64.StdEncoding.DecodeString(encodedValue); err == nil {
return string(decodedBytes)
}
return encodedValue
}
}
}
log.Fatalf("Pattern %s not found in file %s", pattern, filePath)
return ""
}
Loading…
Cancel
Save